@degenerate/generic 0.7.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE.md +12 -60
  2. package/README.md +15 -0
  3. package/dist/_settings/application-settings.d.ts +1 -0
  4. package/dist/_settings/application-settings.d.ts.map +1 -1
  5. package/dist/_settings/application-settings.js +5 -0
  6. package/dist/_settings/authorization-settings.d.ts +2 -2
  7. package/dist/application/database/firestore/generics/add-items-to-database.d.ts.map +1 -1
  8. package/dist/application/database/firestore/generics/add-items-to-database.js +2 -36
  9. package/dist/application/database/firestore/generics/add-or-update-items-to-database.d.ts.map +1 -1
  10. package/dist/application/database/firestore/generics/add-or-update-items-to-database.js +2 -36
  11. package/dist/application/database/firestore/generics/delete-items-on-database.d.ts.map +1 -1
  12. package/dist/application/database/firestore/generics/delete-items-on-database.js +2 -36
  13. package/dist/application/database/firestore/generics/get-item-from-database.d.ts.map +1 -1
  14. package/dist/application/database/firestore/generics/get-item-from-database.js +2 -36
  15. package/dist/application/database/firestore/generics/get-items-different-path-on-database.d.ts.map +1 -1
  16. package/dist/application/database/firestore/generics/get-items-different-path-on-database.js +2 -36
  17. package/dist/application/database/firestore/generics/get-items-from-database.d.ts +9 -4
  18. package/dist/application/database/firestore/generics/get-items-from-database.d.ts.map +1 -1
  19. package/dist/application/database/firestore/generics/get-items-from-database.js +180 -61
  20. package/dist/application/database/firestore/generics/update-items-on-database.d.ts.map +1 -1
  21. package/dist/application/database/firestore/generics/update-items-on-database.js +2 -36
  22. package/dist/application/database/firestore/generics/update-items-properties-on-database.d.ts.map +1 -1
  23. package/dist/application/database/firestore/generics/update-items-properties-on-database.js +2 -36
  24. package/dist/application/database/firestore/page-cursor/page-cursor.d.ts +33 -0
  25. package/dist/application/database/firestore/page-cursor/page-cursor.d.ts.map +1 -0
  26. package/dist/application/database/firestore/page-cursor/page-cursor.js +206 -0
  27. package/dist/application/database/firestore/path-references/audit-log/create-audit-log-col-ref.d.ts.map +1 -1
  28. package/dist/application/database/firestore/path-references/audit-log/create-audit-log-col-ref.js +2 -36
  29. package/dist/application/database/index.d.ts +1 -0
  30. package/dist/application/database/index.d.ts.map +1 -1
  31. package/dist/application/database/index.js +3 -0
  32. package/dist/application/index.d.ts +1 -0
  33. package/dist/application/index.d.ts.map +1 -1
  34. package/dist/application/index.js +3 -0
  35. package/dist/authorization/jwt/sign/create-jwt-options.d.ts +2 -2
  36. package/dist/authorization/jwt/sign/create-jwt-options.d.ts.map +1 -1
  37. package/dist/errors/base-error.d.ts +3 -1
  38. package/dist/errors/base-error.d.ts.map +1 -1
  39. package/dist/errors/base-error.js +5 -1
  40. package/dist/errors/database/database-errors.d.ts +28 -0
  41. package/dist/errors/database/database-errors.d.ts.map +1 -1
  42. package/dist/errors/database/database-errors.js +35 -1
  43. package/dist/helpers/firestore/firestore-id-helpers.d.ts +25 -0
  44. package/dist/helpers/firestore/firestore-id-helpers.d.ts.map +1 -0
  45. package/dist/helpers/firestore/firestore-id-helpers.js +39 -0
  46. package/dist/helpers/index.d.ts +1 -0
  47. package/dist/helpers/index.d.ts.map +1 -1
  48. package/dist/helpers/index.js +1 -0
  49. package/dist/models/application/database/firestore-models.d.ts +35 -14
  50. package/dist/models/application/database/firestore-models.d.ts.map +1 -1
  51. package/dist/models/application/database/firestore-models.js +64 -46
  52. package/package.json +13 -4
@@ -1,87 +1,206 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.getItemsFromDatabase = void 0;
37
- const firebase = __importStar(require("firebase-admin"));
4
+ const firestore_1 = require("firebase-admin/firestore");
38
5
  const application_settings_1 = require("../../../../_settings/application-settings");
6
+ const database_errors_1 = require("../../../../errors/database/database-errors");
39
7
  const application_audit_log_models_1 = require("../../../../models/application/audit-log/application-audit-log-models");
40
8
  const create_audit_log_col_ref_1 = require("../path-references/audit-log/create-audit-log-col-ref");
41
9
  const create_application_audit_log_1 = require("../../../audit-log/create-application-audit-log");
42
- const db = firebase.firestore();
10
+ const page_cursor_1 = require("../page-cursor/page-cursor");
11
+ const firestore_id_helpers_1 = require("../../../../helpers/firestore/firestore-id-helpers");
12
+ // Filters Firestore orders a query by, when no orderBy names their field.
13
+ const INEQUALITY_OPERATORS = new Set(['<', '<=', '>', '>=', '!=', 'not-in']);
14
+ const DOCUMENT_ID_SEGMENTS = ['__name__'];
43
15
  /**
44
- * This function queries the database and returns the items or an empty array.
16
+ * A field path's segments. A dotted string names a nested field, as everywhere in Firestore. A
17
+ * FieldPath's typings don't expose its segments, but every FieldPath carries them; they're read
18
+ * through a checked accessor so all paths order and compare the same way.
19
+ */
20
+ const segmentsOf = (fieldPath, collectionPath) => {
21
+ if (typeof fieldPath === 'string') {
22
+ const segments = fieldPath.split('.');
23
+ if (segments.some((segment) => segment === '')) {
24
+ throw new database_errors_1.DatabaseQueryNotSupportedError(collectionPath, `the field path '${fieldPath}' has an empty segment`);
25
+ }
26
+ return segments;
27
+ }
28
+ const segments = fieldPath.segments;
29
+ if (!Array.isArray(segments) || segments.length === 0 || !segments.every((segment) => typeof segment === 'string')) {
30
+ throw new database_errors_1.DatabaseQueryNotSupportedError(collectionPath, 'a FieldPath whose segments can\'t be read');
31
+ }
32
+ return [...segments];
33
+ };
34
+ const sameSegments = (left, right) => left.length === right.length && left.every((segment, index) => segment === right[index]);
35
+ // Firestore's field path order (its Path.compareTo): segment by segment, numeric `__id<n>__`
36
+ // segments first and by value, the rest in UTF-8 byte order, then the shorter path first. The
37
+ // numeric pattern is stricter than the SDK's (which treats any `__id…__` as a number, and throws
38
+ // on one that isn't): such names are reserved, so only well-formed ones occur.
39
+ const NUMERIC_SEGMENT = /^__id(-?\d+)__$/;
40
+ const compareSegment = (left, right) => {
41
+ const leftNumber = NUMERIC_SEGMENT.exec(left);
42
+ const rightNumber = NUMERIC_SEGMENT.exec(right);
43
+ if (leftNumber && rightNumber) {
44
+ const [a, b] = [BigInt(leftNumber[1]), BigInt(rightNumber[1])];
45
+ return a < b ? -1 : a > b ? 1 : 0;
46
+ }
47
+ if (leftNumber || rightNumber) {
48
+ return leftNumber ? -1 : 1;
49
+ }
50
+ return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'));
51
+ };
52
+ const compareSegments = (left, right) => {
53
+ for (let index = 0; index < Math.min(left.length, right.length); index++) {
54
+ const comparison = compareSegment(left[index], right[index]);
55
+ if (comparison !== 0) {
56
+ return comparison;
57
+ }
58
+ }
59
+ return left.length - right.length;
60
+ };
61
+ const ordering = (segments, direction) => ({
62
+ segments,
63
+ direction,
64
+ path: new firestore_1.FieldPath(...segments),
65
+ isDocumentId: sameSegments(segments, DOCUMENT_ID_SEGMENTS),
66
+ });
67
+ /**
68
+ * The list's orderings, all explicit, so a cursor of values can mark a place in it. They mirror
69
+ * the order Firestore uses implicitly: the orderBy, then every other field an inequality filter
70
+ * uses, sorted by field path, then the document id, which makes the order total. Extra orderings
71
+ * take the orderBy's direction.
72
+ *
73
+ * @throws {DatabaseQueryNotSupportedError} for an order by document id with an inequality filter on
74
+ * another field (Firestore rejects any ordering after the key).
75
+ */
76
+ const listOrderings = (getInformation) => {
77
+ const collectionPath = getInformation.itemColRef.path;
78
+ const direction = getInformation.orderBy?.direction ?? 'asc';
79
+ const orderings = [];
80
+ if (getInformation.orderBy) {
81
+ orderings.push(ordering(segmentsOf(getInformation.orderBy.fieldPath, collectionPath), getInformation.orderBy.direction));
82
+ }
83
+ const inequalityFields = [];
84
+ for (const queryField of getInformation.queryFields) {
85
+ // Every filter's path is checked, so a malformed one is refused here rather than by the SDK.
86
+ const segments = segmentsOf(queryField.fieldPath, collectionPath);
87
+ if (!INEQUALITY_OPERATORS.has(queryField.opStr)) {
88
+ continue;
89
+ }
90
+ const isKnown = sameSegments(segments, DOCUMENT_ID_SEGMENTS)
91
+ || orderings.some((existing) => sameSegments(existing.segments, segments))
92
+ || inequalityFields.some((field) => sameSegments(field, segments));
93
+ if (!isKnown) {
94
+ inequalityFields.push(segments);
95
+ }
96
+ }
97
+ inequalityFields.sort(compareSegments);
98
+ if (orderings[0]?.isDocumentId && inequalityFields.length > 0) {
99
+ // Firestore orders inequality fields after the id, and rejects any ordering after the key.
100
+ throw new database_errors_1.DatabaseQueryNotSupportedError(collectionPath, 'an order by document id with an inequality filter on another field');
101
+ }
102
+ orderings.push(...inequalityFields.map((segments) => ordering(segments, direction)));
103
+ if (!orderings.some((existing) => existing.isDocumentId)) {
104
+ orderings.push(ordering(DOCUMENT_ID_SEGMENTS, direction));
105
+ }
106
+ return orderings;
107
+ };
108
+ /**
109
+ * Whether a cursor's values mark a place in this list: one value per ordering, with a single
110
+ * document id where the list orders by id. The other values are the caller's: they only position
111
+ * the page, and the filters still decide which items it holds, so made-up values reveal nothing.
112
+ */
113
+ const fitsList = (values, orderings) => values.length === orderings.length
114
+ && orderings.every((ordering, index) => !ordering.isDocumentId || (0, firestore_id_helpers_1.isFirestoreDocumentId)(values[index]));
115
+ /**
116
+ * This function queries the database and returns a page of items, and the cursor for the next page.
45
117
  *
46
118
  * @param getInformation the get information (instructions for this function)
47
119
  * @param userId the user ID performing the action
48
120
  * @param applicationAuditLogType the type of audit log to create
49
121
  * @param <T> the type of the items to return
50
- * @returns
122
+ * @returns the page's items, and nextCursor when more items follow
123
+ * @throws {DatabaseQueryNotSupportedError} when the list's filters and order can't be run (a bad
124
+ * field path, or a filter value the SDK refuses while building the query, such as undefined),
125
+ * or the last item's ordered values can't be held in a page cursor. A value the SDK only
126
+ * refuses when it serializes the request (a function, say) surfaces as the SDK's own error.
127
+ * @throws {DatabaseCursorInvalidError} when the cursor is malformed or came from another list.
51
128
  */
52
129
  const getItemsFromDatabase = async (userId, getInformation, applicationAuditLogType = application_audit_log_models_1.ApplicationAuditLogTypeSchema.enum.global) => {
53
- const maxResults = application_settings_1.ApplicationSettings.SystemValues.MaxSnapshotSize; // the maximum number of results to return.
54
- const auditLogColRef = (0, create_audit_log_col_ref_1.createAuditLogColRef)(); // create the audit log collection reference
55
- // get the items from the database.
56
- const itemsToReturn = await db.runTransaction(async (t) => {
57
- const databaseItems = [];
58
- const itemQuery = getInformation.itemColRef;
59
- // add in the query fields if they are set.
130
+ const collectionPath = getInformation.itemColRef.path;
131
+ // the maximum number of results to return: a smaller page for a set of large documents.
132
+ const maxResults = getInformation.isMaxSizeDocumentSet
133
+ ? application_settings_1.ApplicationSettings.SystemValues.MaxSnapshotSizeLargeDocSet
134
+ : application_settings_1.ApplicationSettings.SystemValues.MaxSnapshotSize;
135
+ // The orderings and the cursor depend only on the input, so both are settled before the
136
+ // transaction, and a list or cursor that can't be used is refused without starting one.
137
+ const orderings = listOrderings(getInformation);
138
+ let cursorValues;
139
+ if (getInformation.cursor !== undefined) {
140
+ // References in the cursor belong to the listed collection's own database.
141
+ cursorValues = (0, page_cursor_1.decodePageCursor)(getInformation.cursor, getInformation.itemColRef.firestore, orderings);
142
+ if (cursorValues === undefined || !fitsList(cursorValues, orderings)) {
143
+ throw new database_errors_1.DatabaseCursorInvalidError(collectionPath);
144
+ }
145
+ }
146
+ // Build the whole query up front too. Firestore queries are immutable: where, orderBy,
147
+ // startAfter and limit each return a new Query, which has to be kept (dropping it once ran every
148
+ // list unfiltered, unsorted and unlimited). Filters take a FieldPath built from the checked
149
+ // segments, as orderings do, so a field that can be ordered by can also be filtered on; any
150
+ // filter value or cursor value the SDK refuses becomes a typed error, with no transaction started.
151
+ let itemQuery = getInformation.itemColRef;
152
+ try {
60
153
  for (const queryField of getInformation.queryFields) {
61
- itemQuery.where(queryField.fieldPath, queryField.opStr, queryField.value);
154
+ const path = new firestore_1.FieldPath(...segmentsOf(queryField.fieldPath, collectionPath));
155
+ itemQuery = itemQuery.where(path, queryField.opStr, queryField.value);
62
156
  }
63
- // If order by is set, then order the items.
64
- if (getInformation.orderBy) {
65
- itemQuery.orderBy(getInformation.orderBy.fieldPath, getInformation.orderBy.direction);
157
+ for (const ordering of orderings) {
158
+ itemQuery = itemQuery.orderBy(ordering.path, ordering.direction);
66
159
  }
67
- // add in the query for the pagination.
68
- if (typeof getInformation.lastId !== 'undefined' && getInformation.lastId.length > 0) {
69
- const lastElement = await t.get(itemQuery.doc(getInformation.lastId));
70
- itemQuery.startAfter(lastElement);
160
+ }
161
+ catch (error) {
162
+ throw error instanceof database_errors_1.DatabaseQueryNotSupportedError ? error
163
+ : new database_errors_1.DatabaseQueryNotSupportedError(collectionPath, 'a filter or order Firestore can\'t build', error);
164
+ }
165
+ // Start after the previous page's last item. The cursor holds that item's values, which the
166
+ // caller saw while it was in the list, so no document is read: a page still follows an item
167
+ // deleted or moved out of the list since, and a cursor naming a document outside the filters
168
+ // positions the page without revealing anything about it.
169
+ if (cursorValues !== undefined) {
170
+ try {
171
+ itemQuery = itemQuery.startAfter(...cursorValues);
71
172
  }
72
- // set the limit and query the database.
73
- itemQuery.limit(maxResults);
74
- const itemSnapshot = await t.get(itemQuery);
75
- // get the items from the database.
76
- for (const docSnap of itemSnapshot.docs) {
77
- const item = docSnap.data();
78
- databaseItems.push(item);
173
+ catch (error) {
174
+ // Firestore refuses a value it can't order by; the cursor is the caller's input.
175
+ throw new database_errors_1.DatabaseCursorInvalidError(collectionPath, error);
79
176
  }
177
+ }
178
+ // Read one item past the page: if it exists, more follow, and the page's last item starts
179
+ // the next one. A list that ends exactly at a page boundary gets no cursor.
180
+ itemQuery = itemQuery.limit(maxResults + 1);
181
+ const auditLogColRef = (0, create_audit_log_col_ref_1.createAuditLogColRef)(); // create the audit log collection reference
182
+ // get the items from the database.
183
+ const page = await (0, firestore_1.getFirestore)().runTransaction(async (t) => {
184
+ const itemSnapshot = await t.get(itemQuery);
185
+ const pageDocs = itemSnapshot.docs.slice(0, maxResults);
186
+ const databaseItems = pageDocs.map((docSnap) => docSnap.data());
80
187
  // create the audit log information
81
188
  const auditLog = (0, create_application_audit_log_1.createApplicationAuditLog)(application_audit_log_models_1.ApplicationAuditLogActionSchema.enum.list, getInformation.auditLogSubType, userId, applicationAuditLogType, getInformation.alai, undefined, undefined);
82
189
  t.create(auditLogColRef.doc(), auditLog); // add the audit log to the database.
83
- return databaseItems;
190
+ if (itemSnapshot.docs.length <= maxResults) {
191
+ return { items: databaseItems };
192
+ }
193
+ const lastItem = pageDocs[pageDocs.length - 1];
194
+ const values = orderings.map((ordering) => (ordering.isDocumentId ? lastItem.id : lastItem.get(ordering.path)));
195
+ try {
196
+ return { items: databaseItems, nextCursor: (0, page_cursor_1.encodePageCursor)(values, orderings) };
197
+ }
198
+ catch (error) {
199
+ // A value type the token has no tag for (Enterprise-edition BSON types), or a token
200
+ // over the cap: refuse with a typed error rather than hand out an unusable cursor.
201
+ throw new database_errors_1.DatabaseQueryNotSupportedError(collectionPath, 'the last item\'s ordered values can\'t be held in a page cursor', error);
202
+ }
84
203
  });
85
- return itemsToReturn;
204
+ return page;
86
205
  };
87
206
  exports.getItemsFromDatabase = getItemsFromDatabase;
@@ -1 +1 @@
1
- {"version":3,"file":"update-items-on-database.d.ts","sourceRoot":"","sources":["../../../../../src/application/database/firestore/generics/update-items-on-database.ts"],"names":[],"mappings":"AACA,OAAO,EAEH,uBAAuB,EAE1B,MAAM,uEAAuE,CAAC;AAC/E,OAAO,EAAE,0BAA0B,EAAE,MAAM,0DAA0D,CAAC;AAMtG;;;;;;;GAOG;AACH,QAAA,MAAM,qBAAqB,GAAU,CAAC,EAClC,2BAA2B,0BAA0B,EAAE,EACvD,QAAQ,MAAM,EACd,0BAAyB,uBAAmE,KAC7F,OAAO,CAAC,IAAI,CA0Bd,CAAC;AAEF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
1
+ {"version":3,"file":"update-items-on-database.d.ts","sourceRoot":"","sources":["../../../../../src/application/database/firestore/generics/update-items-on-database.ts"],"names":[],"mappings":"AACA,OAAO,EAEH,uBAAuB,EAE1B,MAAM,uEAAuE,CAAC;AAC/E,OAAO,EAAE,0BAA0B,EAAE,MAAM,0DAA0D,CAAC;AAItG;;;;;;;GAOG;AACH,QAAA,MAAM,qBAAqB,GAAU,CAAC,EAClC,2BAA2B,0BAA0B,EAAE,EACvD,QAAQ,MAAM,EACd,0BAAyB,uBAAmE,KAC7F,OAAO,CAAC,IAAI,CA0Bd,CAAC;AAEF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
@@ -1,45 +1,11 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.updateItemsOnDatabase = void 0;
37
- const admin = __importStar(require("firebase-admin"));
4
+ const firestore_1 = require("firebase-admin/firestore");
38
5
  const application_audit_log_models_1 = require("../../../../models/application/audit-log/application-audit-log-models");
39
6
  const create_audit_log_col_ref_1 = require("../path-references/audit-log/create-audit-log-col-ref");
40
7
  const create_application_audit_log_1 = require("../../../audit-log/create-application-audit-log");
41
8
  const database_errors_1 = require("../../../../errors/database/database-errors");
42
- const db = admin.firestore();
43
9
  /**
44
10
  * This updates the items on the database.
45
11
  *
@@ -50,7 +16,7 @@ const db = admin.firestore();
50
16
  */
51
17
  const updateItemsOnDatabase = async (databaseChangeInformation, userId, applicationAuditLogType = application_audit_log_models_1.ApplicationAuditLogTypeSchema.enum.global) => {
52
18
  // loop the transaction and update the database
53
- await db.runTransaction(async (t) => {
19
+ await (0, firestore_1.getFirestore)().runTransaction(async (t) => {
54
20
  // get the existing records from the database
55
21
  const existingRecords = [];
56
22
  for (const changeRecords of databaseChangeInformation) {
@@ -1 +1 @@
1
- {"version":3,"file":"update-items-properties-on-database.d.ts","sourceRoot":"","sources":["../../../../../src/application/database/firestore/generics/update-items-properties-on-database.ts"],"names":[],"mappings":"AAEA,OAAO,EAGH,uBAAuB,EAE1B,MAAM,uEAAuE,CAAC;AAC/E,OAAO,EAAE,kCAAkC,EAAE,MAAM,0DAA0D,CAAC;AAI9G;;;;;;;;GAQG;AACH,QAAA,MAAM,+BAA+B,GAAU,CAAC,EAC5C,uBAAuB,kCAAkC,EAAE,EAC3D,QAAQ,MAAM,EACd,0BAAyB,uBAAmE,KAC7F,OAAO,CAAC,IAAI,CA6Bd,CAAC;AAEF,OAAO,EAAE,+BAA+B,EAAE,CAAC"}
1
+ {"version":3,"file":"update-items-properties-on-database.d.ts","sourceRoot":"","sources":["../../../../../src/application/database/firestore/generics/update-items-properties-on-database.ts"],"names":[],"mappings":"AAEA,OAAO,EAGH,uBAAuB,EAE1B,MAAM,uEAAuE,CAAC;AAC/E,OAAO,EAAE,kCAAkC,EAAE,MAAM,0DAA0D,CAAC;AAG9G;;;;;;;;GAQG;AACH,QAAA,MAAM,+BAA+B,GAAU,CAAC,EAC5C,uBAAuB,kCAAkC,EAAE,EAC3D,QAAQ,MAAM,EACd,0BAAyB,uBAAmE,KAC7F,OAAO,CAAC,IAAI,CA6Bd,CAAC;AAEF,OAAO,EAAE,+BAA+B,EAAE,CAAC"}
@@ -1,45 +1,11 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.updateItemsPropertiesOnDatabase = void 0;
37
- const admin = __importStar(require("firebase-admin"));
4
+ const firestore_1 = require("firebase-admin/firestore");
38
5
  const database_errors_1 = require("../../../../errors/database/database-errors");
39
6
  const application_audit_log_models_1 = require("../../../../models/application/audit-log/application-audit-log-models");
40
7
  const create_audit_log_col_ref_1 = require("../path-references/audit-log/create-audit-log-col-ref");
41
8
  const create_application_audit_log_1 = require("../../../audit-log/create-application-audit-log");
42
- const db = admin.firestore();
43
9
  /**
44
10
  * This takes a partial object to update and updates the properties on the database. This function is used to update
45
11
  * the properties of an object on the database.
@@ -51,7 +17,7 @@ const db = admin.firestore();
51
17
  */
52
18
  const updateItemsPropertiesOnDatabase = async (databaseChangeRecords, userId, applicationAuditLogType = application_audit_log_models_1.ApplicationAuditLogTypeSchema.enum.global) => {
53
19
  // loop the transaction and update the database
54
- await db.runTransaction(async (t) => {
20
+ await (0, firestore_1.getFirestore)().runTransaction(async (t) => {
55
21
  const auditLogUpdates = []; // create an array of audit log updates
56
22
  const updatedRecord = []; // create an array of updated records
57
23
  // get the existing records from the database
@@ -0,0 +1,33 @@
1
+ import { Firestore } from 'firebase-admin/firestore';
2
+ import { OrderByDirection } from '../../../../models/application/database/firestore-models';
3
+ /**
4
+ * One of a list's orderings, as a cursor records it: the field's path segments and the direction.
5
+ */
6
+ export interface IPageCursorOrdering {
7
+ segments: string[];
8
+ direction: OrderByDirection;
9
+ }
10
+ /**
11
+ * Encodes a page's cursor values as an opaque string token.
12
+ *
13
+ * @param values the last item's value for each of the list's orderings, in order.
14
+ * @param orderings the list's orderings, which the token records so it only fits that list.
15
+ * @returns the token, in URL-safe (base64url) characters; its size depends on the ordered values.
16
+ * @throws {TypeError} when a value is of a type the token can't hold (such as Enterprise-edition
17
+ * BSON types), and {RangeError} when the token would exceed MaxPageCursorLength.
18
+ */
19
+ declare const encodePageCursor: (values: unknown[], orderings: IPageCursorOrdering[]) => string;
20
+ /**
21
+ * Decodes a page cursor token back into its Firestore values.
22
+ *
23
+ * @param token a token from encodePageCursor.
24
+ * @param firestore the Firestore instance document references are rebuilt against.
25
+ * @param orderings the orderings of the list the token is used on.
26
+ * @returns the cursor's values, or undefined when the token is malformed, longer than
27
+ * ApplicationSettings.SystemValues.MaxPageCursorLength, or came from a list with different
28
+ * orderings. The length is checked before parsing, and the orderings and value count before any
29
+ * value is decoded.
30
+ */
31
+ declare const decodePageCursor: (token: string, firestore: Firestore, orderings: IPageCursorOrdering[]) => unknown[] | undefined;
32
+ export { decodePageCursor, encodePageCursor };
33
+ //# sourceMappingURL=page-cursor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-cursor.d.ts","sourceRoot":"","sources":["../../../../../src/application/database/firestore/page-cursor/page-cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,SAAS,EAAuB,MAAM,0BAA0B,CAAC;AAEzG,OAAO,EAAE,gBAAgB,EAAE,MAAM,0DAA0D,CAAC;AAqC5F;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAChC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,gBAAgB,CAAC;CAC/B;AA0ID;;;;;;;;GAQG;AACH,QAAA,MAAM,gBAAgB,GAAI,QAAQ,OAAO,EAAE,EAAE,WAAW,mBAAmB,EAAE,KAAG,MAS/E,CAAC;AAEF;;;;;;;;;;GAUG;AACH,QAAA,MAAM,gBAAgB,GAAI,OAAO,MAAM,EAAE,WAAW,SAAS,EAAE,WAAW,mBAAmB,EAAE,KAAG,OAAO,EAAE,GAAG,SAc7G,CAAC;AAEF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC"}
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encodePageCursor = exports.decodePageCursor = void 0;
4
+ const firestore_1 = require("firebase-admin/firestore");
5
+ const application_settings_1 = require("../../../../_settings/application-settings");
6
+ const firestore_id_helpers_1 = require("../../../../helpers/firestore/firestore-id-helpers");
7
+ // A page cursor travels as an opaque string of URL-safe (base64url) characters; its size depends
8
+ // on the ordered values (see IDatabasePageCursorSchema). Inside it, every Firestore value keeps its type: a Timestamp sent as plain JSON
9
+ // would come back as a map, which Firestore orders after every timestamp, and the page would
10
+ // silently start in the wrong place.
11
+ //
12
+ // The token is base64url JSON: { v: 1, o: <the list's orderings>, values: [...] }. The orderings
13
+ // tie a cursor to the list it came from, so one passed to a differently ordered list is refused
14
+ // rather than silently starting the page in the wrong place. Strings, booleans, null and finite
15
+ // numbers are written as themselves; every other value is a one-key tag object, and a map is
16
+ // always tagged, so any untagged object in a token is malformed.
17
+ const TOKEN_VERSION = 1;
18
+ // Firestore's own limits, so a decoded value the backend would reject is refused as a bad cursor
19
+ // up front rather than failing later with a raw error: maps and arrays nest at most 20 deep, and a
20
+ // vector holds at most 2048 numbers.
21
+ const MAX_DEPTH = 20;
22
+ const MAX_VECTOR_LENGTH = 2048;
23
+ // Map keys, strings and ids are checked with the shared Firestore id helpers: a cursor can only
24
+ // hold names and text read from a stored document, so a reserved or empty key, a lone surrogate or
25
+ // an invalid id is forged, and refusing it here matters because Firestore fails some of them with
26
+ // an error a transaction retries for minutes.
27
+ // A document path: collection and id pairs, each a valid id. A cursor can only hold a reference
28
+ // read from a stored document, so any other path is forged.
29
+ const isDocumentPath = (path) => {
30
+ const segments = path.split('/');
31
+ return segments.length % 2 === 0 && segments.every(firestore_id_helpers_1.isFirestoreDocumentId);
32
+ };
33
+ // Firestore integers are 64-bit.
34
+ const MIN_INT64 = -(2n ** 63n);
35
+ const MAX_INT64 = 2n ** 63n - 1n;
36
+ const orderingsKey = (orderings) => JSON.stringify(orderings.map((ordering) => [ordering.segments, ordering.direction]));
37
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype;
38
+ // firebase-admin doesn't export VectorValue, so it's recognised by its shape.
39
+ const isVectorValue = (value) => typeof value === 'object' && value !== null && value.constructor?.name === 'VectorValue'
40
+ && typeof value.toArray === 'function';
41
+ const encodeValue = (value) => {
42
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') {
43
+ return value;
44
+ }
45
+ if (typeof value === 'bigint') {
46
+ return { $int: value.toString() }; // what Firestore returns for an integer with useBigInt
47
+ }
48
+ if (typeof value === 'number') {
49
+ if (Object.is(value, -0)) {
50
+ return { $num: '-0' }; // String(-0) is '0', and JSON writes -0 as 0
51
+ }
52
+ return Number.isFinite(value) ? value : { $num: String(value) };
53
+ }
54
+ if (value instanceof firestore_1.Timestamp) {
55
+ return { $ts: [value.seconds, value.nanoseconds] };
56
+ }
57
+ if (value instanceof firestore_1.GeoPoint) {
58
+ return { $geo: [value.latitude, value.longitude] };
59
+ }
60
+ if (value instanceof firestore_1.DocumentReference) {
61
+ return { $ref: value.path };
62
+ }
63
+ if (value instanceof Uint8Array) {
64
+ return { $bytes: Buffer.from(value).toString('base64') };
65
+ }
66
+ if (isVectorValue(value)) {
67
+ return { $vec: value.toArray().map(encodeValue) }; // numbers, so ±Infinity and -0 keep their $num tags
68
+ }
69
+ if (Array.isArray(value)) {
70
+ return value.map(encodeValue);
71
+ }
72
+ if (isPlainObject(value)) {
73
+ return { $map: Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, encodeValue(entry)])) };
74
+ }
75
+ throw new TypeError(`A page cursor can't hold a value of type ${typeof value}`);
76
+ };
77
+ const isNumberPair = (value) => Array.isArray(value) && value.length === 2 && value.every((entry) => typeof entry === 'number');
78
+ // A key a stored map can hold: not empty, not reserved, and well-formed.
79
+ const isAllowedMapKey = (key) => key !== '' && !(0, firestore_id_helpers_1.isReservedFirestoreName)(key) && (0, firestore_id_helpers_1.isWellFormedText)(key);
80
+ const decodeValue = (encoded, firestore, depth = 0) => {
81
+ if (depth > MAX_DEPTH) {
82
+ throw new TypeError('nested too deep');
83
+ }
84
+ if (typeof encoded === 'string') {
85
+ if (!(0, firestore_id_helpers_1.isWellFormedText)(encoded)) {
86
+ throw new TypeError('malformed string');
87
+ }
88
+ return encoded;
89
+ }
90
+ if (encoded === null || typeof encoded === 'boolean' || typeof encoded === 'number') {
91
+ return encoded;
92
+ }
93
+ if (Array.isArray(encoded)) {
94
+ return encoded.map((entry) => decodeValue(entry, firestore, depth + 1));
95
+ }
96
+ if (!isPlainObject(encoded) || Object.keys(encoded).length !== 1) {
97
+ throw new TypeError('malformed value');
98
+ }
99
+ const [[tag, body]] = Object.entries(encoded);
100
+ switch (tag) {
101
+ case '$num':
102
+ if (body === 'NaN' || body === 'Infinity' || body === '-Infinity' || body === '-0') {
103
+ return Number(body);
104
+ }
105
+ break;
106
+ case '$int':
107
+ if (typeof body === 'string' && /^-?\d{1,20}$/.test(body)) {
108
+ const value = BigInt(body);
109
+ if (value >= MIN_INT64 && value <= MAX_INT64) {
110
+ return value;
111
+ }
112
+ }
113
+ break;
114
+ case '$ts':
115
+ if (isNumberPair(body)) {
116
+ return new firestore_1.Timestamp(body[0], body[1]);
117
+ }
118
+ break;
119
+ case '$geo':
120
+ if (isNumberPair(body)) {
121
+ return new firestore_1.GeoPoint(body[0], body[1]);
122
+ }
123
+ break;
124
+ case '$ref':
125
+ if (typeof body === 'string' && isDocumentPath(body)) {
126
+ return firestore.doc(body);
127
+ }
128
+ break;
129
+ case '$bytes':
130
+ if (typeof body === 'string') {
131
+ return Buffer.from(body, 'base64');
132
+ }
133
+ break;
134
+ case '$vec':
135
+ if (Array.isArray(body) && body.length >= 1 && body.length <= MAX_VECTOR_LENGTH) {
136
+ // An element is a number or a `$num` tag, nothing else, so a vector can't nest
137
+ // anything (or recurse past MAX_DEPTH); NaN can't be stored, so it's refused.
138
+ return firestore_1.FieldValue.vector(body.map((entry) => {
139
+ const isNumberTag = isPlainObject(entry) && Object.keys(entry).length === 1 && '$num' in entry;
140
+ const element = typeof entry === 'number' || isNumberTag
141
+ // a scalar, so it sits at the vector's depth; the shape check rules out nesting
142
+ ? decodeValue(entry, firestore, depth)
143
+ : undefined;
144
+ if (typeof element !== 'number' || Number.isNaN(element)) {
145
+ throw new TypeError('malformed vector element');
146
+ }
147
+ return element;
148
+ }));
149
+ }
150
+ break;
151
+ case '$map':
152
+ if (isPlainObject(body) && Object.keys(body).every(isAllowedMapKey)) {
153
+ return Object.fromEntries(Object.entries(body).map(([key, entry]) => [key, decodeValue(entry, firestore, depth + 1)]));
154
+ }
155
+ break;
156
+ }
157
+ throw new TypeError('malformed value');
158
+ };
159
+ /**
160
+ * Encodes a page's cursor values as an opaque string token.
161
+ *
162
+ * @param values the last item's value for each of the list's orderings, in order.
163
+ * @param orderings the list's orderings, which the token records so it only fits that list.
164
+ * @returns the token, in URL-safe (base64url) characters; its size depends on the ordered values.
165
+ * @throws {TypeError} when a value is of a type the token can't hold (such as Enterprise-edition
166
+ * BSON types), and {RangeError} when the token would exceed MaxPageCursorLength.
167
+ */
168
+ const encodePageCursor = (values, orderings) => {
169
+ const token = Buffer.from(JSON.stringify({ v: TOKEN_VERSION, o: orderingsKey(orderings), values: values.map(encodeValue) }))
170
+ .toString('base64url');
171
+ if (token.length > application_settings_1.ApplicationSettings.SystemValues.MaxPageCursorLength) {
172
+ // Rare but reachable (an ordered value Firestore stores small that encodes large, such as
173
+ // millions of empty Bytes): never issue a cursor the next call would refuse.
174
+ throw new RangeError('A page cursor would be longer than MaxPageCursorLength');
175
+ }
176
+ return token;
177
+ };
178
+ exports.encodePageCursor = encodePageCursor;
179
+ /**
180
+ * Decodes a page cursor token back into its Firestore values.
181
+ *
182
+ * @param token a token from encodePageCursor.
183
+ * @param firestore the Firestore instance document references are rebuilt against.
184
+ * @param orderings the orderings of the list the token is used on.
185
+ * @returns the cursor's values, or undefined when the token is malformed, longer than
186
+ * ApplicationSettings.SystemValues.MaxPageCursorLength, or came from a list with different
187
+ * orderings. The length is checked before parsing, and the orderings and value count before any
188
+ * value is decoded.
189
+ */
190
+ const decodePageCursor = (token, firestore, orderings) => {
191
+ if (token.length > application_settings_1.ApplicationSettings.SystemValues.MaxPageCursorLength) {
192
+ return undefined;
193
+ }
194
+ try {
195
+ const parsed = JSON.parse(Buffer.from(token, 'base64url').toString('utf8'));
196
+ if (!isPlainObject(parsed) || parsed.v !== TOKEN_VERSION || parsed.o !== orderingsKey(orderings)
197
+ || !Array.isArray(parsed.values) || parsed.values.length !== orderings.length) {
198
+ return undefined;
199
+ }
200
+ return parsed.values.map((entry) => decodeValue(entry, firestore));
201
+ }
202
+ catch {
203
+ return undefined;
204
+ }
205
+ };
206
+ exports.decodePageCursor = decodePageCursor;
@@ -1 +1 @@
1
- {"version":3,"file":"create-audit-log-col-ref.d.ts","sourceRoot":"","sources":["../../../../../../src/application/database/firestore/path-references/audit-log/create-audit-log-col-ref.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAI/D;;;;GAIG;AACH,QAAA,MAAM,oBAAoB,QAAO,mBAEhC,CAAC;AAEF,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
1
+ {"version":3,"file":"create-audit-log-col-ref.d.ts","sourceRoot":"","sources":["../../../../../../src/application/database/firestore/path-references/audit-log/create-audit-log-col-ref.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAgB,MAAM,0BAA0B,CAAC;AAE7E;;;;GAIG;AACH,QAAA,MAAM,oBAAoB,QAAO,mBAEhC,CAAC;AAEF,OAAO,EAAE,oBAAoB,EAAE,CAAC"}