Skip to content

Commit f9e8130

Browse files
authored
Merge pull request #3 from GitLiveApp/dynamic-target-collection-path
feat(deleteReferences): Add dynamic parameters
2 parents f0d7a09 + e383701 commit f9e8130

File tree

5 files changed

+649
-31
lines changed

5 files changed

+649
-31
lines changed

src/common.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,49 @@ export function isRule(arg: Rule | Config): arg is Rule {
1717
export function isConfig(arg: Rule | Config): arg is Config {
1818
return (arg as Config).config !== undefined;
1919
}
20+
21+
enum Key {
22+
Primary = '{(.*?)}',
23+
Foreign = '([$][^/]*|$)',
24+
}
25+
26+
function regexMatches(text: string, regex: Key): string[] {
27+
return text.match(new RegExp(regex, 'g')) || [];
28+
}
29+
30+
export function getPrimaryKey(
31+
ref: string
32+
): { hasPrimaryKey: boolean; primaryKey: string } {
33+
const keys = regexMatches(ref, Key.Primary);
34+
if (keys.length > 0) {
35+
const pk = keys.pop(); // Pop the last item in the matched array
36+
// Remove { } from the primary key
37+
return { hasPrimaryKey: true, primaryKey: pk.replace(/\{|\}/g, '') };
38+
}
39+
return { hasPrimaryKey: false, primaryKey: 'masterId' };
40+
}
41+
42+
export function replaceReferencesWith(
43+
fields: FirebaseFirestore.DocumentData,
44+
targetCollection: string
45+
): { hasFields: boolean; targetCollection: string } {
46+
const matches = regexMatches(targetCollection, Key.Foreign);
47+
matches.pop(); // The foreign key regex always return '' at the end
48+
let hasFields = false;
49+
if (matches.length > 0 && fields) {
50+
hasFields = true;
51+
matches.forEach(match => {
52+
const field = fields[match.replace('$', '')];
53+
if (field) {
54+
console.log(
55+
`integrify: Detected dynamic reference, replacing [${match}] with [${field}]`
56+
);
57+
targetCollection = targetCollection.replace(match, field);
58+
} else {
59+
throw new Error(`integrify: Missing dynamic reference: [${match}]`);
60+
}
61+
});
62+
}
63+
64+
return { hasFields, targetCollection };
65+
}

src/rules/deleteReferences.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Config, Rule } from '../common';
1+
import { Config, Rule, replaceReferencesWith, getPrimaryKey } from '../common';
22

33
export interface DeleteReferencesRule extends Rule {
44
source: {
@@ -30,12 +30,24 @@ export function integrifyDeleteReferences(
3030
)
3131
);
3232

33+
const { hasPrimaryKey, primaryKey } = getPrimaryKey(rule.source.collection);
34+
if (!hasPrimaryKey) {
35+
rule.source.collection = `${rule.source.collection}/{${primaryKey}}`;
36+
}
37+
3338
return functions.firestore
34-
.document(`${rule.source.collection}/{masterId}`)
39+
.document(rule.source.collection)
3540
.onDelete((snap, context) => {
36-
const masterId = context.params.masterId;
41+
// Get the last {...} in the source collection
42+
const primaryKeyValue = context.params[primaryKey];
43+
if (!primaryKeyValue) {
44+
throw new Error(
45+
`integrify: Missing a primary key [${primaryKey}] in the source params`
46+
);
47+
}
48+
3749
console.log(
38-
`integrify: Detected delete in [${rule.source.collection}], id [${masterId}]`
50+
`integrify: Detected delete in [${rule.source.collection}], id [${primaryKeyValue}]`
3951
);
4052

4153
// Call "pre" hook if defined
@@ -53,18 +65,33 @@ export function integrifyDeleteReferences(
5365
target.isCollectionGroup ? 'group ' : ''
5466
}[${target.collection}] where foreign key [${
5567
target.foreignKey
56-
}] matches [${masterId}]`
68+
}] matches [${primaryKeyValue}]`
69+
);
70+
71+
// Replace the context.params in the target collection
72+
const paramSwap = replaceReferencesWith(
73+
context.params,
74+
target.collection
75+
);
76+
77+
// Replace the snapshot fields in the target collection
78+
const fieldSwap = replaceReferencesWith(
79+
snap.data(),
80+
paramSwap.targetCollection
5781
);
82+
target.collection = fieldSwap.targetCollection;
83+
5884
// Delete all docs in this target corresponding to deleted master doc
5985
let whereable = null;
6086
if (target.isCollectionGroup) {
6187
whereable = db.collectionGroup(target.collection);
6288
} else {
6389
whereable = db.collection(target.collection);
6490
}
91+
6592
promises.push(
6693
whereable
67-
.where(target.foreignKey, '==', masterId)
94+
.where(target.foreignKey, '==', primaryKeyValue)
6895
.get()
6996
.then(querySnap => {
7097
querySnap.forEach(doc => {

test/functions/index.js

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ module.exports.replicateMasterToDetail = integrify({
4242
module.exports.deleteReferencesToMaster = integrify({
4343
rule: 'DELETE_REFERENCES',
4444
source: {
45-
collection: 'master',
45+
collection: 'master/{masterId}',
4646
},
4747
targets: [
4848
{
@@ -62,6 +62,102 @@ module.exports.deleteReferencesToMaster = integrify({
6262
},
6363
});
6464

65+
module.exports.deleteReferencesWithMasterParam = integrify({
66+
rule: 'DELETE_REFERENCES',
67+
source: {
68+
collection: 'master/{primaryKey}',
69+
},
70+
targets: [
71+
{
72+
collection: 'detail1',
73+
foreignKey: 'primaryKey',
74+
},
75+
{
76+
collection: 'somecoll/$primaryKey/detail2',
77+
foreignKey: 'primaryKey',
78+
},
79+
],
80+
hooks: {
81+
pre: (snap, context) => {
82+
setState({
83+
snap,
84+
context,
85+
});
86+
},
87+
},
88+
});
89+
90+
module.exports.deleteReferencesWithSnapshotFields = integrify({
91+
rule: 'DELETE_REFERENCES',
92+
source: {
93+
collection: 'master/{anotherId}',
94+
},
95+
targets: [
96+
{
97+
collection: 'detail1',
98+
foreignKey: 'anotherId',
99+
},
100+
{
101+
collection: 'somecoll/$testId/detail2',
102+
foreignKey: 'anotherId',
103+
},
104+
],
105+
hooks: {
106+
pre: (snap, context) => {
107+
setState({
108+
snap,
109+
context,
110+
});
111+
},
112+
},
113+
});
114+
115+
module.exports.deleteReferencesWithMissingKey = integrify({
116+
rule: 'DELETE_REFERENCES',
117+
source: {
118+
collection: 'master',
119+
},
120+
targets: [
121+
{
122+
collection: 'detail1',
123+
foreignKey: 'randomId',
124+
},
125+
],
126+
hooks: {
127+
pre: (snap, context) => {
128+
setState({
129+
snap,
130+
context,
131+
});
132+
},
133+
},
134+
});
135+
136+
module.exports.deleteReferencesWithMissingFields = integrify({
137+
rule: 'DELETE_REFERENCES',
138+
source: {
139+
collection: 'master/{randomId}',
140+
},
141+
targets: [
142+
{
143+
collection: 'detail1',
144+
foreignKey: 'randomId',
145+
},
146+
{
147+
collection: 'somecoll/$testId/detail2',
148+
foreignKey: 'randomId',
149+
},
150+
],
151+
hooks: {
152+
pre: (snap, context) => {
153+
setState({
154+
snap,
155+
context,
156+
});
157+
},
158+
},
159+
});
160+
65161
module.exports.maintainFavoritesCount = integrify({
66162
rule: 'MAINTAIN_COUNT',
67163
source: {

test/functions/integrify.rules.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ module.exports = [
3131
rule: 'DELETE_REFERENCES',
3232
name: 'deleteReferencesToMaster',
3333
source: {
34-
collection: 'master',
34+
collection: 'master/{masterId}',
3535
},
3636
targets: [
3737
{
@@ -45,6 +45,70 @@ module.exports = [
4545
},
4646
],
4747
},
48+
{
49+
rule: 'DELETE_REFERENCES',
50+
name: 'deleteReferencesWithMasterParam',
51+
source: {
52+
collection: 'master/{primaryKey}',
53+
},
54+
targets: [
55+
{
56+
collection: 'detail1',
57+
foreignKey: 'primaryKey',
58+
},
59+
{
60+
collection: 'somecoll/$primaryKey/detail2',
61+
foreignKey: 'primaryKey',
62+
},
63+
],
64+
},
65+
{
66+
rule: 'DELETE_REFERENCES',
67+
name: 'deleteReferencesWithSnapshotFields',
68+
source: {
69+
collection: 'master/{anotherId}',
70+
},
71+
targets: [
72+
{
73+
collection: 'detail1',
74+
foreignKey: 'anotherId',
75+
},
76+
{
77+
collection: 'somecoll/$testId/detail2',
78+
foreignKey: 'anotherId',
79+
},
80+
],
81+
},
82+
{
83+
rule: 'DELETE_REFERENCES',
84+
name: 'deleteReferencesWithMissingKey',
85+
source: {
86+
collection: 'master',
87+
},
88+
targets: [
89+
{
90+
collection: 'detail1',
91+
foreignKey: 'randomId',
92+
},
93+
],
94+
},
95+
{
96+
rule: 'DELETE_REFERENCES',
97+
name: 'deleteReferencesWithMissingFields',
98+
source: {
99+
collection: 'master/{randomId}',
100+
},
101+
targets: [
102+
{
103+
collection: 'detail1',
104+
foreignKey: 'randomId',
105+
},
106+
{
107+
collection: 'somecoll/$testId/detail2',
108+
foreignKey: 'randomId',
109+
},
110+
],
111+
},
48112
{
49113
rule: 'MAINTAIN_COUNT',
50114
name: 'maintainFavoritesCount',

0 commit comments

Comments
 (0)