@aglyn/plugins-data 1.0.0-beta.220 → 1.0.0-beta.222

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 (135) hide show
  1. package/README.md +9 -6
  2. package/package.json +10 -10
  3. package/src/index.d.ts +1 -1
  4. package/src/index.js +1 -1
  5. package/src/index.js.map +1 -1
  6. package/src/lib/artifact/dataset-schema-artifact.server.d.ts +57 -0
  7. package/src/lib/artifact/dataset-schema-artifact.server.js +308 -0
  8. package/src/lib/artifact/dataset-schema-artifact.server.js.map +1 -0
  9. package/src/lib/artifact/dataset-schema.d.ts +136 -0
  10. package/src/lib/artifact/dataset-schema.js +232 -0
  11. package/src/lib/artifact/dataset-schema.js.map +1 -0
  12. package/src/lib/components/dataset-record-dialog.component.d.ts +1 -1
  13. package/src/lib/components/dataset-record-dialog.component.js +1 -1
  14. package/src/lib/components/dataset-record-dialog.component.js.map +1 -1
  15. package/src/lib/components/dataset-record-filter.d.ts +1 -1
  16. package/src/lib/components/dataset-record-filter.js +2 -2
  17. package/src/lib/components/dataset-record-filter.js.map +1 -1
  18. package/src/lib/components/dataset-schema-dialog.component.d.ts +1 -1
  19. package/src/lib/components/dataset-schema-dialog.component.js +55 -23
  20. package/src/lib/components/dataset-schema-dialog.component.js.map +1 -1
  21. package/src/lib/components/host-datasets-card.component.js +91 -403
  22. package/src/lib/components/host-datasets-card.component.js.map +1 -1
  23. package/src/lib/constants/bundle-common.d.ts +12 -0
  24. package/src/lib/constants/bundle-common.js +13 -0
  25. package/src/lib/constants/bundle-common.js.map +1 -1
  26. package/src/lib/declarations.console-server.d.ts +16 -1
  27. package/src/lib/declarations.console-server.js +34 -2
  28. package/src/lib/declarations.console-server.js.map +1 -1
  29. package/src/lib/declarations.server.d.ts +13 -0
  30. package/src/lib/declarations.server.js +48 -2
  31. package/src/lib/declarations.server.js.map +1 -1
  32. package/src/lib/form-target/dataset-form-record-target.server.js +5 -3
  33. package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -1
  34. package/src/lib/model/dataset-models.d.ts +392 -0
  35. package/src/lib/model/dataset-models.js +806 -0
  36. package/src/lib/model/dataset-models.js.map +1 -0
  37. package/src/lib/model/dataset-record-view.d.ts +1 -1
  38. package/src/lib/model/dataset-record-view.js +1 -2
  39. package/src/lib/model/dataset-record-view.js.map +1 -1
  40. package/src/lib/model/dataset-record.js +2 -2
  41. package/src/lib/model/dataset-record.js.map +1 -1
  42. package/src/lib/model/datasets.d.ts +162 -0
  43. package/src/lib/model/datasets.js +227 -0
  44. package/src/lib/model/datasets.js.map +1 -0
  45. package/src/lib/plugin.d.ts +4 -0
  46. package/src/lib/plugin.js +40 -0
  47. package/src/lib/plugin.js.map +1 -1
  48. package/src/lib/record-pages/record-page-live-paths.server.d.ts +55 -0
  49. package/src/lib/record-pages/record-page-live-paths.server.js +83 -0
  50. package/src/lib/record-pages/record-page-live-paths.server.js.map +1 -0
  51. package/src/lib/record-pages/record-page-read.server.d.ts +70 -0
  52. package/src/lib/record-pages/record-page-read.server.js +175 -0
  53. package/src/lib/record-pages/record-page-read.server.js.map +1 -0
  54. package/src/lib/record-pages/record-page-resolver.server.d.ts +37 -0
  55. package/src/lib/record-pages/record-page-resolver.server.js +131 -0
  56. package/src/lib/record-pages/record-page-resolver.server.js.map +1 -0
  57. package/src/lib/record-pages/record-page-sitemap.server.d.ts +32 -0
  58. package/src/lib/record-pages/record-page-sitemap.server.js +102 -0
  59. package/src/lib/record-pages/record-page-sitemap.server.js.map +1 -0
  60. package/src/lib/record-pages/record-page-source.d.ts +42 -0
  61. package/src/lib/record-pages/record-page-source.js +176 -0
  62. package/src/lib/record-pages/record-page-source.js.map +1 -0
  63. package/src/lib/record-pages/record-pages-route.d.ts +42 -0
  64. package/src/lib/record-pages/record-pages-route.js +348 -0
  65. package/src/lib/record-pages/record-pages-route.js.map +1 -0
  66. package/src/lib/record-pages/record-pages.d.ts +218 -0
  67. package/src/lib/record-pages/record-pages.js +351 -0
  68. package/src/lib/record-pages/record-pages.js.map +1 -0
  69. package/src/lib/record-pages/record-template-row-chip.component.d.ts +28 -0
  70. package/src/lib/record-pages/record-template-row-chip.component.js +87 -0
  71. package/src/lib/record-pages/record-template-row-chip.component.js.map +1 -0
  72. package/src/lib/record-pages/record-template-section.component.d.ts +30 -0
  73. package/src/lib/record-pages/record-template-section.component.js +491 -0
  74. package/src/lib/record-pages/record-template-section.component.js.map +1 -0
  75. package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -1
  76. package/src/lib/repeat/dataset-repeat-rows.js +30 -12
  77. package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
  78. package/src/lib/repeat/dataset-repeat-rows.server.d.ts +1 -1
  79. package/src/lib/repeat/dataset-repeat-rows.server.js +23 -12
  80. package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -1
  81. package/src/lib/repeat/repeat-record-pages.d.ts +48 -0
  82. package/src/lib/repeat/repeat-record-pages.js +64 -0
  83. package/src/lib/repeat/repeat-record-pages.js.map +1 -0
  84. package/src/lib/server/announce-dataset-records.d.ts +1 -1
  85. package/src/lib/server/announce-dataset-records.js +1 -1
  86. package/src/lib/server/announce-dataset-records.js.map +1 -1
  87. package/src/lib/server/api-v1/datasets.js +5 -3
  88. package/src/lib/server/api-v1/datasets.js.map +1 -1
  89. package/src/lib/server/dataset-figures.js +2 -2
  90. package/src/lib/server/dataset-figures.js.map +1 -1
  91. package/src/lib/server/dataset-live-pages.d.ts +147 -0
  92. package/src/lib/server/dataset-live-pages.js +436 -0
  93. package/src/lib/server/dataset-live-pages.js.map +1 -0
  94. package/src/lib/server/dataset-steps.server.d.ts +45 -0
  95. package/src/lib/server/dataset-steps.server.js +268 -0
  96. package/src/lib/server/dataset-steps.server.js.map +1 -0
  97. package/src/lib/server/datasets-route.d.ts +2 -0
  98. package/src/lib/server/datasets-route.js +102 -6
  99. package/src/lib/server/datasets-route.js.map +1 -1
  100. package/src/lib/server/resolve-dataset.d.ts +40 -0
  101. package/src/lib/server/resolve-dataset.js +64 -0
  102. package/src/lib/server/resolve-dataset.js.map +1 -0
  103. package/src/lib/server.d.ts +3 -2
  104. package/src/lib/server.js +8 -9
  105. package/src/lib/server.js.map +1 -1
  106. package/src/lib/site-bundle/datasets-package.d.ts +42 -0
  107. package/src/lib/site-bundle/datasets-package.js +95 -0
  108. package/src/lib/site-bundle/datasets-package.js.map +1 -0
  109. package/src/lib/site-bundle/datasets-site-bundle.server.js +2 -2
  110. package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -1
  111. package/src/lib/{model/index.js → tenant-api.d.ts} +12 -7
  112. package/src/lib/tenant-api.js +30 -0
  113. package/src/lib/tenant-api.js.map +1 -0
  114. package/src/lib/transfer/dataset-transfer-key.d.ts +30 -0
  115. package/src/lib/transfer/dataset-transfer-key.js +31 -0
  116. package/src/lib/transfer/dataset-transfer-key.js.map +1 -0
  117. package/src/lib/transfer/dataset-transfer-model.d.ts +146 -0
  118. package/src/lib/transfer/dataset-transfer-model.js +536 -0
  119. package/src/lib/transfer/dataset-transfer-model.js.map +1 -0
  120. package/src/lib/{model/index.d.ts → transfer/dataset-transfer-resource.d.ts} +2 -6
  121. package/src/lib/transfer/dataset-transfer-resource.js +41 -0
  122. package/src/lib/transfer/dataset-transfer-resource.js.map +1 -0
  123. package/src/lib/transfer/dataset-transfer.server.d.ts +69 -0
  124. package/src/lib/transfer/dataset-transfer.server.js +930 -0
  125. package/src/lib/transfer/dataset-transfer.server.js.map +1 -0
  126. package/src/lib/model/dataset-csv.d.ts +0 -36
  127. package/src/lib/model/dataset-csv.js +0 -85
  128. package/src/lib/model/dataset-csv.js.map +0 -1
  129. package/src/lib/model/dataset-io.d.ts +0 -44
  130. package/src/lib/model/dataset-io.js +0 -85
  131. package/src/lib/model/dataset-io.js.map +0 -1
  132. package/src/lib/model/index.js.map +0 -1
  133. package/src/lib/server/datasets-export-route.d.ts +0 -57
  134. package/src/lib/server/datasets-export-route.js +0 -229
  135. package/src/lib/server/datasets-export-route.js.map +0 -1
@@ -0,0 +1,930 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { createHash } from "node:crypto";
18
+ import { TRANSFER_ID_FIELD, buildTransferPlan, matchLookupKey, planTransferUndo, rankTransferLookupSuggestions, transferLookupKey, transferLookupNewName, transferResourceInstanceOf } from "@aglyn/aglyn/data-transfer";
19
+ import { checkQuota, ensureDeclaredCustomFieldTypes, memberCanSee } from "@aglyn/aglyn/server";
20
+ import { dataStorageRefusal, firebaseAdmin, isServerReleaseFlagOnForOrg, resolveOrgMembership } from "@aglyn/tenant-data-admin";
21
+ import { applyListQuery } from "@aglyn/tenant-data-admin/server/list-query";
22
+ import { TransferEngineError } from "@aglyn/tenant-data-admin/server/transfer-jobs";
23
+ import { FieldPath, FieldValue, Timestamp } from "firebase-admin/firestore";
24
+ import { datasetRecordFilter, planRecordQuery } from "../components/dataset-record-filter.js";
25
+ import { datasetFilterToken, datasetFilterValuePath, datasetFilterWords, datasetIntegrityFields, datasetIntegrityUpdate, effectiveDatasetModel } from "../model/dataset-models.js";
26
+ import { datasetDisplayName, describeDatasetRecordErrors } from "../model/datasets.js";
27
+ import { fillRecordAddresses } from "../record-pages/record-pages.js";
28
+ import { announceDatasetRecords } from "../server/announce-dataset-records.js";
29
+ import { datasetTransferResourceKey } from "./dataset-transfer-key.js";
30
+ import { datasetFieldIdOf, datasetFieldOfPicklist, datasetHoldsKey, datasetMatchKeyOffer, datasetMatchKeyQuery, datasetOptionsPicklist, datasetRecordTransferValues, datasetStorageValues, datasetTransferCatalog, datasetTransferFieldId, datasetWriteErrors, isOptionsField, limitDatasetCreates, refuseInvalidDatasetRows } from "./dataset-transfer-model.js";
31
+ const productionDeps = ()=>({
32
+ firestore: firebaseAdmin.app().firestore()
33
+ });
34
+ /** The page an export reads when the engine names none. */ const PAGE_ROWS = 500;
35
+ /** Firestore's cap on the values of one `in` query. */ const IN_MAX = 30;
36
+ /** The most documents one `getAll` reads. */ const GET_ALL_MAX = 300;
37
+ /** What a merging record write names, so each field is replaced whole. */ const RECORD_MERGE_FIELDS = [
38
+ 'values',
39
+ 'referencedIds',
40
+ 'filterKeys',
41
+ 'filterValues',
42
+ 'updatedAt'
43
+ ];
44
+ const chunked = (items, size)=>{
45
+ const out = [];
46
+ for(let at = 0; at < items.length; at += size)out.push(items.slice(at, at + size));
47
+ return out;
48
+ };
49
+ const notFound = ()=>new TransferEngineError('notFound', 404, 'No such dataset.');
50
+ /** The nanoid alphabet `createResourceUid` names documents from. */ const ID_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';
51
+ /**
52
+ * The id a record created by an import's row is given: ten characters of the
53
+ * resource-id alphabet, derived from the job and the row, so a retried chunk
54
+ * names the record it already wrote rather than writing a second one.
55
+ */ export function transferRecordId(jobId, row) {
56
+ const digest = createHash('sha256').update(`${jobId}:${row}`).digest();
57
+ let id = '';
58
+ for(let at = 0; at < 10; at += 1)id += ID_ALPHABET[digest[at] % ID_ALPHABET.length];
59
+ return id;
60
+ }
61
+ /**
62
+ * The dataset per context object: the engine hands one context to every
63
+ * hook a step runs, so an export's pages and a plan's lookups read the
64
+ * dataset and the member once.
65
+ */ const loaded = new WeakMap();
66
+ async function readDataset(deps, ctx) {
67
+ const datasetId = transferResourceInstanceOf(ctx);
68
+ if (!datasetId) throw notFound();
69
+ const orgRef = deps.firestore.collection('orgs').doc(ctx.orgId);
70
+ const ref = orgRef.collection('datasets').doc(datasetId);
71
+ const snapshot = await ref.get();
72
+ const data = snapshot.exists ? snapshot.data() : null;
73
+ if (!data || data['deletedAt'] != null) throw notFound();
74
+ const visibleTo = Array.isArray(data['visibleTo']) ? data['visibleTo'] : [];
75
+ /*
76
+ * The member has to SEE the dataset, as on every other door onto its
77
+ * records (`datasets-route.ts`): the gate asked about the workspace, and a
78
+ * collaborator scoped to one site passes it. The Admin SDK reads past the
79
+ * rules, so this IS the enforcement, answered with the same 404 as a
80
+ * dataset that does not exist. No membership is staff (the gate admitted
81
+ * them) or the sweep resuming a job; the data store's release flag holds
82
+ * for everyone else.
83
+ */ if (ctx.actorUid) {
84
+ var _this;
85
+ const member = (_this = await resolveOrgMembership(ctx.actorUid, ctx.orgId)) == null ? void 0 : _this.member;
86
+ if (member) {
87
+ if (!memberCanSee(member, visibleTo)) throw notFound();
88
+ if (!await isServerReleaseFlagOnForOrg('release_data_store', ctx.orgId)) {
89
+ throw new TransferEngineError('notFound', 404, 'Datasets are not available on this workspace.');
90
+ }
91
+ }
92
+ }
93
+ return {
94
+ id: datasetId,
95
+ label: datasetDisplayName(data) || 'Fields',
96
+ model: effectiveDatasetModel(data),
97
+ visibleTo,
98
+ orgRef,
99
+ ref,
100
+ records: ref.collection('records')
101
+ };
102
+ }
103
+ function datasetFor(deps, ctx) {
104
+ let pending = loaded.get(ctx);
105
+ if (!pending) {
106
+ pending = readDataset(deps, ctx);
107
+ loaded.set(ctx, pending);
108
+ pending.catch(()=>loaded.delete(ctx));
109
+ }
110
+ return pending;
111
+ }
112
+ /** The context a referenced dataset is asked in: the same job, actor and site, as that dataset. */ function targetContext(ctx, datasetId) {
113
+ return _extends({}, ctx, {
114
+ resource: datasetTransferResourceKey(datasetId)
115
+ });
116
+ }
117
+ /**
118
+ * Each dataset the model references, as the person may use it: whether they
119
+ * can read it (the same check as opening it), and its model when they can.
120
+ */ async function datasetReferenceTargets(deps, ctx, model) {
121
+ var _model_fields;
122
+ const out = {};
123
+ for (const field of Object.values((_model_fields = model.fields) != null ? _model_fields : {})){
124
+ var _field_reference;
125
+ const targetId = (field == null ? void 0 : field.type) === 'reference' ? (_field_reference = field.reference) == null ? void 0 : _field_reference.datasetId : undefined;
126
+ if (!targetId || out[targetId]) continue;
127
+ try {
128
+ out[targetId] = {
129
+ readable: true,
130
+ model: (await datasetFor(deps, targetContext(ctx, targetId))).model
131
+ };
132
+ } catch (error) {
133
+ if (!(error instanceof TransferEngineError && error.code === 'notFound')) throw error;
134
+ out[targetId] = {
135
+ readable: false
136
+ };
137
+ }
138
+ }
139
+ return out;
140
+ }
141
+ /** A record document as transfer values. */ function viewOf(model, doc) {
142
+ var _doc_data, _data_values;
143
+ const data = (_doc_data = doc.data()) != null ? _doc_data : {};
144
+ return datasetRecordTransferValues(model, {
145
+ id: doc.id,
146
+ values: (_data_values = data['values']) != null ? _data_values : {},
147
+ createdAt: data['createdAt'],
148
+ updatedAt: data['updatedAt']
149
+ });
150
+ }
151
+ async function getAll(firestore, refs) {
152
+ const out = [];
153
+ for (const slice of chunked(refs, GET_ALL_MAX))if (slice.length) out.push(...await firestore.getAll(...slice));
154
+ return out;
155
+ }
156
+ function readFilter(filter) {
157
+ const clauses = Array.isArray(filter['clauses']) ? filter['clauses'] : [];
158
+ const search = Array.isArray(filter['search']) ? filter['search'] : [];
159
+ const clause = (entry)=>{
160
+ const one = entry;
161
+ return Boolean(one) && typeof (one == null ? void 0 : one['field']) === 'string' && typeof (one == null ? void 0 : one['op']) === 'string' && typeof (one == null ? void 0 : one['value']) === 'string';
162
+ };
163
+ if (!clauses.every(clause) || !search.every((word)=>typeof word === 'string')) {
164
+ throw new TransferEngineError('invalid', 400, 'The filter could not be read.');
165
+ }
166
+ return {
167
+ clauses: clauses,
168
+ search: search
169
+ };
170
+ }
171
+ /**
172
+ * Every record, or the ones the records table's filter matches — the same
173
+ * one query the table pages (`planRecordQuery`), ordered by document id.
174
+ * What the table could not put on its query it did not apply either, so
175
+ * neither does the export.
176
+ */ function recordsQuery(dataset, options) {
177
+ if (!(options == null ? void 0 : options.filter)) return dataset.records.orderBy(FieldPath.documentId());
178
+ const filter = readFilter(options.filter);
179
+ const { plan } = planRecordQuery(dataset.model, datasetRecordFilter(dataset.model), filter.clauses, filter.search);
180
+ return applyListQuery(dataset.records, plan);
181
+ }
182
+ /** A collaborator's scope tokens must reach the dataset; records carry no scope of their own. */ function assertScope(dataset, options) {
183
+ if ((options == null ? void 0 : options.scopeTokens) && !options.scopeTokens.some((token)=>dataset.visibleTo.includes(token))) throw notFound();
184
+ }
185
+ function pick(view, fieldIds) {
186
+ return Object.fromEntries(fieldIds.filter((fieldId)=>view[fieldId] !== undefined).map((fieldId)=>[
187
+ fieldId,
188
+ view[fieldId]
189
+ ]));
190
+ }
191
+ export async function countDatasetRecords(deps, ctx, options) {
192
+ var _data_count;
193
+ const dataset = await datasetFor(deps, ctx);
194
+ assertScope(dataset, options);
195
+ if (options.ids) {
196
+ let total = 0;
197
+ for (const slice of chunked(options.ids, IN_MAX)){
198
+ var _data_count1;
199
+ total += Number((_data_count1 = (await dataset.records.where(FieldPath.documentId(), 'in', slice).count().get()).data().count) != null ? _data_count1 : 0);
200
+ }
201
+ return total;
202
+ }
203
+ return Number((_data_count = (await recordsQuery(dataset, options).count().get()).data().count) != null ? _data_count : 0);
204
+ }
205
+ export async function readDatasetPage(deps, ctx, cursor, fieldIds, options) {
206
+ var _ref;
207
+ const dataset = await datasetFor(deps, ctx);
208
+ assertScope(dataset, options);
209
+ const size = Math.max(1, Math.min((_ref = options == null ? void 0 : options.pageSize) != null ? _ref : PAGE_ROWS, PAGE_ROWS));
210
+ if (options == null ? void 0 : options.ids) {
211
+ // The selection, in the order it was made; the cursor is how far through it.
212
+ const offset = Math.max(0, Number(cursor != null ? cursor : 0) || 0);
213
+ const slice = options.ids.slice(offset, offset + size);
214
+ const docs = await getAll(deps.firestore, slice.map((id)=>dataset.records.doc(id)));
215
+ return {
216
+ rows: docs.filter((doc)=>doc.exists).map((doc)=>pick(viewOf(dataset.model, doc), fieldIds)),
217
+ next: offset + size < options.ids.length ? String(offset + size) : null
218
+ };
219
+ }
220
+ let query = recordsQuery(dataset, options);
221
+ if (cursor) query = query.startAfter(cursor);
222
+ const snapshot = await query.limit(size).get();
223
+ const docs = snapshot.docs;
224
+ return {
225
+ rows: docs.map((doc)=>pick(viewOf(dataset.model, doc), fieldIds)),
226
+ next: docs.length === size ? docs[docs.length - 1].id : null
227
+ };
228
+ }
229
+ /*------------------------------------------
230
+ * Matching
231
+ *-----------------------------------------*/ export async function lookupDatasetRecords(deps, ctx, requests) {
232
+ const dataset = await datasetFor(deps, ctx);
233
+ const lookup = new Map();
234
+ const records = new Map();
235
+ const file = (fieldId, normalized, id)=>{
236
+ var _lookup_get;
237
+ const at = matchLookupKey(fieldId, normalized);
238
+ const ids = (_lookup_get = lookup.get(at)) != null ? _lookup_get : [];
239
+ if (!ids.includes(id)) ids.push(id);
240
+ lookup.set(at, ids);
241
+ };
242
+ for (const request of requests){
243
+ var _datasetFieldIdOf;
244
+ if (!request.values.length) continue;
245
+ if (request.fieldId === TRANSFER_ID_FIELD) {
246
+ const docs = await getAll(deps.firestore, request.values.map((id)=>dataset.records.doc(id)));
247
+ for (const doc of docs){
248
+ if (!doc.exists) continue;
249
+ records.set(doc.id, viewOf(dataset.model, doc));
250
+ file(request.fieldId, doc.id, doc.id);
251
+ }
252
+ continue;
253
+ }
254
+ // Asked of `filterValues`, the index the records table's own filters
255
+ // read; a value clipped there is held to the whole value below.
256
+ const asked = new Map();
257
+ for (const normalized of request.values){
258
+ var _asked_get;
259
+ const query = datasetMatchKeyQuery(dataset.model, request, normalized);
260
+ if (!query) continue;
261
+ asked.set(query.value, [
262
+ ...(_asked_get = asked.get(query.value)) != null ? _asked_get : [],
263
+ normalized
264
+ ]);
265
+ }
266
+ const path = datasetFilterValuePath((_datasetFieldIdOf = datasetFieldIdOf(request.fieldId)) != null ? _datasetFieldIdOf : '');
267
+ if (!path) continue;
268
+ for (const slice of chunked([
269
+ ...asked.keys()
270
+ ], IN_MAX)){
271
+ const snapshot = await dataset.records.where(path, 'in', slice).get();
272
+ for (const doc of snapshot.docs){
273
+ const view = viewOf(dataset.model, doc);
274
+ let held = false;
275
+ for (const normalized of slice.flatMap((value)=>{
276
+ var _asked_get;
277
+ return (_asked_get = asked.get(value)) != null ? _asked_get : [];
278
+ })){
279
+ if (!datasetHoldsKey(view, request, normalized)) continue;
280
+ file(request.fieldId, normalized, doc.id);
281
+ held = true;
282
+ }
283
+ if (held) records.set(doc.id, view);
284
+ }
285
+ }
286
+ }
287
+ return {
288
+ lookup,
289
+ records
290
+ };
291
+ }
292
+ /** The most distinct word queries one suggestion request runs. */ const SUGGEST_QUERIES_MAX = 200;
293
+ /** The records one word query reads. */ const SUGGEST_READ = 20;
294
+ /** The words of a value its candidates are asked by. */ const SUGGEST_WORDS = 2;
295
+ /** The leading characters a word is also asked by, so a later misspelling still finds it. */ const SUGGEST_STEM = 3;
296
+ /**
297
+ * Records named LIKE each value a reference could not find, for the person
298
+ * to pick from: each plain text `by` field asked for records with a word that
299
+ * starts with one of the value's first words, or with its first few letters
300
+ * (the `filterKeys` prefix tokens the records table's `contains` reads), then
301
+ * ranked by how close the whole name is. Values past the query budget get none.
302
+ */ export async function suggestDatasetRecords(deps, ctx, request) {
303
+ const dataset = await datasetFor(deps, ctx);
304
+ const fields = request.by.map((transferId)=>datasetFieldIdOf(transferId)).filter((fieldId)=>{
305
+ var _dataset_model_fields;
306
+ const field = fieldId ? (_dataset_model_fields = dataset.model.fields) == null ? void 0 : _dataset_model_fields[fieldId] : undefined;
307
+ return (field == null ? void 0 : field.type) === 'text' && !isOptionsField(field);
308
+ });
309
+ const asked = new Map();
310
+ const ask = (fieldId, token)=>{
311
+ const at = `${fieldId}\u0000${token}`;
312
+ let pending = asked.get(at);
313
+ if (!pending) {
314
+ pending = dataset.records.where('filterKeys', 'array-contains', token).limit(SUGGEST_READ).get().then((snapshot)=>snapshot.docs.map((doc)=>{
315
+ var _fieldId, _doc_get;
316
+ return {
317
+ recordId: doc.id,
318
+ label: String((_fieldId = ((_doc_get = doc.get('values')) != null ? _doc_get : {})[fieldId]) != null ? _fieldId : '').trim()
319
+ };
320
+ }));
321
+ asked.set(at, pending);
322
+ }
323
+ return pending;
324
+ };
325
+ const answer = {};
326
+ for (const value of request.values){
327
+ const candidates = [];
328
+ for (const fieldId of fields){
329
+ for (const word of datasetFilterWords(value).slice(0, SUGGEST_WORDS)){
330
+ const stem = Array.from(word).slice(0, SUGGEST_STEM).join('');
331
+ for (const asWord of new Set([
332
+ word,
333
+ stem
334
+ ])){
335
+ const token = datasetFilterToken(dataset.model, {
336
+ field: fieldId,
337
+ op: 'contains',
338
+ value: asWord
339
+ });
340
+ if (!token || asked.size >= SUGGEST_QUERIES_MAX && !asked.has(`${fieldId}\u0000${token}`)) continue;
341
+ candidates.push(...(await ask(fieldId, token)).filter((candidate)=>candidate.label));
342
+ }
343
+ }
344
+ }
345
+ answer[value] = rankTransferLookupSuggestions(value, candidates, {
346
+ limit: 5
347
+ });
348
+ }
349
+ return answer;
350
+ }
351
+ /*------------------------------------------
352
+ * Options fields as picklists
353
+ *-----------------------------------------*/ export async function datasetPicklists(deps, ctx, picklistIds) {
354
+ const { model } = await datasetFor(deps, ctx);
355
+ const out = {};
356
+ for (const picklistId of picklistIds){
357
+ var _datasetFieldOfPicklist;
358
+ var _model_fields;
359
+ const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[(_datasetFieldOfPicklist = datasetFieldOfPicklist(picklistId)) != null ? _datasetFieldOfPicklist : ''];
360
+ if (field) out[picklistId] = datasetOptionsPicklist(field);
361
+ }
362
+ return out;
363
+ }
364
+ /**
365
+ * The values the person chose to add to an options field, added to the
366
+ * field's options in the dataset's model — the schema the Schema dialog
367
+ * edits — before the import's first write. A value already there is left.
368
+ */ export async function addDatasetOptions(deps, ctx, picklistId, values) {
369
+ const dataset = await datasetFor(deps, ctx);
370
+ const fieldId = datasetFieldOfPicklist(picklistId);
371
+ if (!fieldId) return;
372
+ await deps.firestore.runTransaction(async (tx)=>{
373
+ var _snapshot_data, _ref;
374
+ var _model_fields, _field_validation;
375
+ const snapshot = await tx.get(dataset.ref);
376
+ const model = effectiveDatasetModel((_snapshot_data = snapshot.data()) != null ? _snapshot_data : {});
377
+ const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[fieldId];
378
+ if (!field) return;
379
+ const options = [
380
+ ...(_ref = (_field_validation = field.validation) == null ? void 0 : _field_validation.options) != null ? _ref : []
381
+ ];
382
+ const added = values.map((value)=>value.label).filter((label)=>label && !options.includes(label));
383
+ if (!added.length) return;
384
+ tx.update(dataset.ref, {
385
+ model: _extends({}, model, {
386
+ fields: _extends({}, model.fields, {
387
+ [fieldId]: _extends({}, field, {
388
+ validation: _extends({}, field.validation, {
389
+ options: [
390
+ ...options,
391
+ ...added
392
+ ]
393
+ })
394
+ })
395
+ })
396
+ }),
397
+ updatedAt: Timestamp.now()
398
+ });
399
+ });
400
+ // The writes that follow validate against the options just added.
401
+ loaded.delete(ctx);
402
+ }
403
+ /*------------------------------------------
404
+ * The dry run
405
+ *-----------------------------------------*/ /**
406
+ * The records this import may create, from the plan's `recordsPerDataset`
407
+ * against the dataset's real size — and none while the data storage band is
408
+ * full — so the dry run fails what Apply would refuse.
409
+ */ async function createLimits(dataset) {
410
+ var _data_count;
411
+ const org = (await dataset.orgRef.get()).data();
412
+ if (await dataStorageRefusal(org, dataset.orgRef)) return {
413
+ maxCreates: 0
414
+ };
415
+ const live = Number((_data_count = (await dataset.records.count().get()).data().count) != null ? _data_count : 0);
416
+ const { remaining } = checkQuota(org, 'recordsPerDataset', live);
417
+ return Number.isFinite(remaining) ? {
418
+ maxCreates: remaining
419
+ } : {};
420
+ }
421
+ const failed = (row, reason, message)=>({
422
+ result: _extends({
423
+ row: row.index,
424
+ outcome: 'failed'
425
+ }, row.recordId ? {
426
+ recordId: row.recordId
427
+ } : {}, {
428
+ reason
429
+ }, message ? {
430
+ message
431
+ } : {})
432
+ });
433
+ function only(view, fieldIds) {
434
+ return Object.fromEntries(fieldIds.map((fieldId)=>{
435
+ var _view_fieldId;
436
+ return [
437
+ fieldId,
438
+ (_view_fieldId = view[fieldId]) != null ? _view_fieldId : null
439
+ ];
440
+ }));
441
+ }
442
+ /** Updates: each record's values with the row's changes over them, in one batch. */ async function applyUpdates(deps, dataset, rows) {
443
+ if (!rows.length) return [];
444
+ const { model, records } = dataset;
445
+ const docs = await getAll(deps.firestore, rows.map((row)=>records.doc(row.recordId)));
446
+ const batch = deps.firestore.batch();
447
+ const landed = [];
448
+ let writes = 0;
449
+ rows.forEach((row, at)=>{
450
+ var _doc_get;
451
+ const doc = docs[at];
452
+ if (!doc.exists) return landed.push(failed(row, 'matchedRecordMissing'));
453
+ const stored = _extends({}, (_doc_get = doc.get('values')) != null ? _doc_get : {});
454
+ const before = viewOf(model, doc);
455
+ const { values: changes, cleared } = datasetStorageValues(model, Object.fromEntries(row.diff.map((change)=>[
456
+ change.fieldId,
457
+ change.after
458
+ ])));
459
+ const merged = _extends({}, stored, changes);
460
+ for (const fieldId of cleared)delete merged[fieldId];
461
+ const coerced = fillRecordAddresses(model, merged);
462
+ const errors = datasetWriteErrors(model, coerced, new Set([
463
+ ...Object.keys(changes),
464
+ ...cleared
465
+ ]));
466
+ if (Object.keys(errors).length) return landed.push(failed(row, 'refusedValue', describeDatasetRecordErrors(errors)));
467
+ batch.set(doc.ref, _extends({
468
+ values: coerced
469
+ }, datasetIntegrityUpdate(model, coerced, FieldValue.delete()), {
470
+ updatedAt: Timestamp.now()
471
+ }), // Each named field replaced whole: `merge: true` would fold the new
472
+ // `filterValues` into the stored map and keep a cleared value.
473
+ {
474
+ mergeFields: RECORD_MERGE_FIELDS
475
+ });
476
+ writes += 1;
477
+ const touched = row.diff.map((change)=>change.fieldId);
478
+ const after = datasetRecordTransferValues(model, {
479
+ values: coerced
480
+ });
481
+ landed.push({
482
+ result: {
483
+ row: row.index,
484
+ outcome: 'updated',
485
+ recordId: doc.id
486
+ },
487
+ undo: {
488
+ row: row.index,
489
+ recordId: doc.id,
490
+ action: 'updated',
491
+ previous: only(before, touched),
492
+ written: only(after, touched),
493
+ modes: Object.fromEntries(row.diff.map((change)=>[
494
+ change.fieldId,
495
+ change.mode
496
+ ]))
497
+ }
498
+ });
499
+ return undefined;
500
+ });
501
+ if (writes) await batch.commit();
502
+ return landed;
503
+ }
504
+ /**
505
+ * New records written as `/api/orgs/datasets` creates them: the data storage
506
+ * band refusing first, then counted, decided and written in ONE transaction
507
+ * against `recordsPerDataset`, each with the integrity index of its values.
508
+ * A record already there is left as it is. `values` are validated already.
509
+ */ async function writeNewRecords(deps, dataset, entries) {
510
+ const { model, records } = dataset;
511
+ const org = (await dataset.orgRef.get()).data();
512
+ // Bytes, not rows (AGL-2163), before the write as on every other create.
513
+ const storage = await dataStorageRefusal(org, dataset.orgRef);
514
+ if (storage) return {
515
+ message: `Dataset storage is full (${storage.includedMb} MB on this plan) — upgrade in Billing`
516
+ };
517
+ const outcomes = await deps.firestore.runTransaction(async (tx)=>{
518
+ var _data_count;
519
+ const refs = entries.map((entry)=>records.doc(entry.id));
520
+ const existing = await tx.getAll(...refs);
521
+ const live = Number((_data_count = (await tx.get(records.count())).data().count) != null ? _data_count : 0);
522
+ let room = checkQuota(org, 'recordsPerDataset', live).remaining;
523
+ let order = live;
524
+ return entries.map((entry, at)=>{
525
+ if (existing[at].exists) return 'existed';
526
+ if (room <= 0) return 'refused';
527
+ room -= 1;
528
+ tx.create(refs[at], _extends({
529
+ values: entry.values
530
+ }, datasetIntegrityFields(model, entry.values), {
531
+ order: order++,
532
+ createdAt: Timestamp.now(),
533
+ updatedAt: Timestamp.now()
534
+ }));
535
+ return 'created';
536
+ });
537
+ });
538
+ const limit = checkQuota(org, 'recordsPerDataset', 0).limit;
539
+ return {
540
+ outcomes,
541
+ limitMessage: `Record limit reached (${limit}) — upgrade in Billing`
542
+ };
543
+ }
544
+ /**
545
+ * Creates, through {@link writeNewRecords}: validated first, so a row the
546
+ * model refuses fails naming why. A row the plan's room has run out for
547
+ * fails as past the plan; a record this job already created (a retried
548
+ * chunk) is reported, not written again.
549
+ */ async function applyCreates(deps, ctx, dataset, rows) {
550
+ if (!rows.length) return [];
551
+ const { model } = dataset;
552
+ const landed = [];
553
+ const prepared = [];
554
+ for (const row of rows){
555
+ var _ctx_jobId;
556
+ const { values } = datasetStorageValues(model, Object.fromEntries(row.diff.map((change)=>[
557
+ change.fieldId,
558
+ change.after
559
+ ])));
560
+ const coerced = fillRecordAddresses(model, values);
561
+ const errors = datasetWriteErrors(model, coerced);
562
+ if (Object.keys(errors).length) landed.push(failed(row, 'refusedValue', describeDatasetRecordErrors(errors)));
563
+ else prepared.push({
564
+ row,
565
+ id: transferRecordId((_ctx_jobId = ctx.jobId) != null ? _ctx_jobId : ctx.resource, row.index),
566
+ values: coerced
567
+ });
568
+ }
569
+ if (!prepared.length) return landed;
570
+ const written = await writeNewRecords(deps, dataset, prepared);
571
+ if ('message' in written) return [
572
+ ...landed,
573
+ ...prepared.map((entry)=>failed(entry.row, 'planLimit', written.message))
574
+ ];
575
+ prepared.forEach((entry, at)=>{
576
+ if (written.outcomes[at] === 'refused') {
577
+ landed.push(failed(entry.row, 'planLimit', written.limitMessage));
578
+ return;
579
+ }
580
+ const values = datasetRecordTransferValues(model, {
581
+ values: entry.values
582
+ });
583
+ delete values[TRANSFER_ID_FIELD];
584
+ landed.push({
585
+ result: {
586
+ row: entry.row.index,
587
+ outcome: 'created',
588
+ recordId: entry.id
589
+ },
590
+ undo: {
591
+ row: entry.row.index,
592
+ recordId: entry.id,
593
+ action: 'created',
594
+ written: values
595
+ }
596
+ });
597
+ });
598
+ return landed;
599
+ }
600
+ /*------------------------------------------
601
+ * References the person chose to create
602
+ *-----------------------------------------*/ /**
603
+ * The id a record created for a reference's name is given: ten characters
604
+ * of the resource-id alphabet, from the job, the target, the field it is
605
+ * named in and the name as the engine keys it — so every row naming it, in
606
+ * any chunk, and a retried chunk all land on the one record.
607
+ */ export function transferReferenceRecordId(jobId, datasetId, displayFieldId, name) {
608
+ return transferRecordId(`${jobId}:${datasetId}:${displayFieldId}:${transferLookupKey(name)}`, 0);
609
+ }
610
+ /** The names a reference value asks to create, by the engine's marker. */ function namesToCreate(value) {
611
+ const names = [];
612
+ for (const item of Array.isArray(value) ? value : [
613
+ value
614
+ ]){
615
+ const name = transferLookupNewName(item);
616
+ if (name) names.push(name);
617
+ }
618
+ return names;
619
+ }
620
+ function namedReferences(model) {
621
+ var _model_order;
622
+ const out = [];
623
+ for (const fieldId of (_model_order = model.order) != null ? _model_order : []){
624
+ var _model_fields, _field_reference, _field_reference1;
625
+ const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[fieldId];
626
+ const targetId = (field == null ? void 0 : field.type) === 'reference' ? (_field_reference = field.reference) == null ? void 0 : _field_reference.datasetId : undefined;
627
+ const displayFieldId = field == null ? void 0 : (_field_reference1 = field.reference) == null ? void 0 : _field_reference1.displayFieldId;
628
+ if (targetId && displayFieldId) out.push({
629
+ transferId: datasetTransferFieldId(fieldId),
630
+ targetId,
631
+ displayFieldId
632
+ });
633
+ }
634
+ return out;
635
+ }
636
+ /**
637
+ * The records the person chose to create from a reference's names, created
638
+ * in the referenced dataset before the rows that name them are written: each
639
+ * name once, holding the name in the field the reference displays, held to
640
+ * the target's model and its plan, through {@link writeNewRecords}. The rows
641
+ * come back naming the records' ids; a row naming one that could not be
642
+ * created fails, saying why.
643
+ */ async function createNamedReferences(deps, ctx, dataset, rows) {
644
+ var _ctx_jobId;
645
+ const references = namedReferences(dataset.model);
646
+ // Target and display field → name key → the name as the file spelled it.
647
+ const wanted = new Map();
648
+ for (const row of rows){
649
+ for (const change of row.diff){
650
+ var _wanted_get;
651
+ const reference = references.find((one)=>one.transferId === change.fieldId);
652
+ const names = reference ? namesToCreate(change.after) : [];
653
+ if (!reference || !names.length) continue;
654
+ const at = `${reference.targetId}\u0000${reference.displayFieldId}`;
655
+ const group = (_wanted_get = wanted.get(at)) != null ? _wanted_get : {
656
+ reference,
657
+ names: new Map()
658
+ };
659
+ for (const name of names)if (!group.names.has(transferLookupKey(name))) group.names.set(transferLookupKey(name), name);
660
+ wanted.set(at, group);
661
+ }
662
+ }
663
+ if (!wanted.size) return {
664
+ rows: [
665
+ ...rows
666
+ ],
667
+ failed: []
668
+ };
669
+ // Name key → the record's id, or why it could not be created; per target and display field.
670
+ const outcome = new Map();
671
+ const jobId = (_ctx_jobId = ctx.jobId) != null ? _ctx_jobId : ctx.resource;
672
+ for (const [at, { reference, names }] of wanted){
673
+ const decided = new Map();
674
+ outcome.set(at, decided);
675
+ const target = await datasetFor(deps, targetContext(ctx, reference.targetId));
676
+ await ensureDeclaredCustomFieldTypes(target.model);
677
+ const entries = [];
678
+ for (const [key, name] of names){
679
+ const values = fillRecordAddresses(target.model, datasetStorageValues(target.model, {
680
+ [datasetTransferFieldId(reference.displayFieldId)]: name
681
+ }).values);
682
+ const errors = datasetWriteErrors(target.model, values);
683
+ if (Object.keys(errors).length) {
684
+ decided.set(key, {
685
+ message: `“${name}” could not be created in ${target.label}: ${describeDatasetRecordErrors(errors)}`
686
+ });
687
+ } else {
688
+ entries.push({
689
+ key,
690
+ id: transferReferenceRecordId(jobId, target.id, reference.displayFieldId, name),
691
+ values
692
+ });
693
+ }
694
+ }
695
+ if (!entries.length) continue;
696
+ const written = await writeNewRecords(deps, target, entries);
697
+ entries.forEach((entry, index)=>{
698
+ const name = names.get(entry.key);
699
+ if ('message' in written) decided.set(entry.key, {
700
+ message: `“${name}” could not be created in ${target.label}: ${written.message}`
701
+ });
702
+ else if (written.outcomes[index] === 'refused') {
703
+ decided.set(entry.key, {
704
+ message: `“${name}” could not be created in ${target.label}: ${written.limitMessage}`
705
+ });
706
+ } else decided.set(entry.key, {
707
+ id: entry.id
708
+ });
709
+ });
710
+ if (!('message' in written) && written.outcomes.includes('created')) {
711
+ await announceDatasetRecords({
712
+ firestore: deps.firestore,
713
+ orgId: ctx.orgId,
714
+ datasetId: target.id
715
+ });
716
+ }
717
+ }
718
+ const out = [];
719
+ const refused = [];
720
+ for (const row of rows){
721
+ const problems = [];
722
+ const diff = row.diff.map((change)=>{
723
+ const reference = references.find((one)=>one.transferId === change.fieldId);
724
+ if (!reference || !namesToCreate(change.after).length) return change;
725
+ const decided = outcome.get(`${reference.targetId}\u0000${reference.displayFieldId}`);
726
+ const idOf = (item)=>{
727
+ const name = transferLookupNewName(item);
728
+ if (!name) return item;
729
+ const one = decided == null ? void 0 : decided.get(transferLookupKey(name));
730
+ if (one && 'id' in one) return one.id;
731
+ problems.push(one && 'message' in one ? one.message : `“${name}” could not be created.`);
732
+ return item;
733
+ };
734
+ return _extends({}, change, {
735
+ after: Array.isArray(change.after) ? [
736
+ ...new Set(change.after.map(idOf))
737
+ ] : idOf(change.after)
738
+ });
739
+ });
740
+ if (problems.length) refused.push(failed(row, 'refusedValue', [
741
+ ...new Set(problems)
742
+ ].join(' ')));
743
+ else out.push(_extends({}, row, {
744
+ diff
745
+ }));
746
+ }
747
+ return {
748
+ rows: out,
749
+ failed: refused
750
+ };
751
+ }
752
+ export async function applyDatasetChunk(deps, ctx, chunk, writer) {
753
+ const dataset = await datasetFor(deps, ctx);
754
+ // A plugin's field validator runs only once its plugin registered it.
755
+ await ensureDeclaredCustomFieldTypes(dataset.model);
756
+ const results = [];
757
+ const undo = [];
758
+ const pending = [];
759
+ for (const row of chunk.rows){
760
+ const done = await writer.alreadyApplied(row.index);
761
+ if (done) results.push(done);
762
+ else pending.push(row);
763
+ }
764
+ // The records the rows' references name for creation, before the rows.
765
+ const named = await createNamedReferences(deps, ctx, dataset, pending);
766
+ const landed = [
767
+ ...named.failed,
768
+ ...await applyUpdates(deps, dataset, named.rows.filter((row)=>row.verdict === 'update' && row.recordId)),
769
+ ...await applyCreates(deps, ctx, dataset, named.rows.filter((row)=>row.verdict === 'create'))
770
+ ];
771
+ for (const entry of landed){
772
+ await writer.markApplied(entry.result, entry.undo);
773
+ results.push(entry.result);
774
+ if (entry.undo) undo.push(entry.undo);
775
+ }
776
+ // The pages repeating over the dataset, told once for the chunk (AGL-3113).
777
+ if (undo.length) {
778
+ await announceDatasetRecords({
779
+ firestore: deps.firestore,
780
+ orgId: ctx.orgId,
781
+ datasetId: dataset.id
782
+ });
783
+ }
784
+ return {
785
+ results,
786
+ undo
787
+ };
788
+ }
789
+ /*------------------------------------------
790
+ * Undo
791
+ *-----------------------------------------*/ /**
792
+ * Deletes a record the import created, keeping every reference to it whole
793
+ * (see the block header). `false` when a `restrict` reference holds it.
794
+ */ async function deleteWithIntegrity(deps, dataset, recordId) {
795
+ const others = await dataset.orgRef.collection('datasets').get();
796
+ const strips = [];
797
+ const batch = deps.firestore.batch();
798
+ for (const other of others.docs){
799
+ var _model_fields;
800
+ const model = effectiveDatasetModel(other.data());
801
+ const referencing = Object.keys((_model_fields = model.fields) != null ? _model_fields : {}).filter((fieldId)=>{
802
+ var _model_fields_fieldId, _model_fields_fieldId_reference, _model_fields_fieldId1;
803
+ return ((_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId.type) === 'reference' && ((_model_fields_fieldId1 = model.fields[fieldId]) == null ? void 0 : (_model_fields_fieldId_reference = _model_fields_fieldId1.reference) == null ? void 0 : _model_fields_fieldId_reference.datasetId) === dataset.id;
804
+ });
805
+ if (!referencing.length) continue;
806
+ const holders = (await other.ref.collection('records').where('referencedIds', 'array-contains', recordId).get()).docs.filter((doc)=>referencing.some((fieldId)=>{
807
+ var _doc_get;
808
+ const stored = ((_doc_get = doc.get('values')) != null ? _doc_get : {})[fieldId];
809
+ return Array.isArray(stored) ? stored.includes(recordId) : stored === recordId;
810
+ }));
811
+ if (!holders.length) continue;
812
+ if (referencing.some((fieldId)=>{
813
+ var _model_fields_fieldId_reference, _model_fields_fieldId;
814
+ return ((_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : (_model_fields_fieldId_reference = _model_fields_fieldId.reference) == null ? void 0 : _model_fields_fieldId_reference.onDelete) === 'restrict';
815
+ })) return false;
816
+ for (const holder of holders){
817
+ strips.push(()=>{
818
+ var _holder_get;
819
+ const values = _extends({}, (_holder_get = holder.get('values')) != null ? _holder_get : {});
820
+ for (const fieldId of referencing){
821
+ const stored = values[fieldId];
822
+ if (Array.isArray(stored)) values[fieldId] = stored.filter((id)=>id !== recordId);
823
+ else if (stored === recordId) delete values[fieldId];
824
+ }
825
+ batch.update(holder.ref, _extends({
826
+ values
827
+ }, datasetIntegrityUpdate(model, values, FieldValue.delete()), {
828
+ updatedAt: Timestamp.now()
829
+ }));
830
+ });
831
+ }
832
+ }
833
+ for (const strip of strips)strip();
834
+ batch.delete(dataset.records.doc(recordId));
835
+ await batch.commit();
836
+ return true;
837
+ }
838
+ export async function revertDatasetChunk(deps, ctx, snapshot, decisions = {}) {
839
+ const dataset = await datasetFor(deps, ctx);
840
+ const { model, records } = dataset;
841
+ const done = [];
842
+ const conflicts = [];
843
+ if (!snapshot.entries.length) return {
844
+ done,
845
+ conflicts
846
+ };
847
+ const docs = await getAll(deps.firestore, snapshot.entries.map((entry)=>records.doc(entry.recordId)));
848
+ const restores = deps.firestore.batch();
849
+ let restoring = 0;
850
+ for (const [at, entry] of snapshot.entries.entries()){
851
+ var _doc_get;
852
+ const doc = docs[at];
853
+ const step = planTransferUndo(entry, doc.exists ? viewOf(model, doc) : null);
854
+ const revert = step.action !== 'conflict' || decisions[entry.recordId] === 'revert';
855
+ if (!revert) {
856
+ conflicts.push(step);
857
+ continue;
858
+ }
859
+ if (step.action === 'nothing') {
860
+ done.push(step);
861
+ continue;
862
+ }
863
+ if (step.action === 'delete' || step.action === 'conflict' && entry.action === 'created') {
864
+ if (await deleteWithIntegrity(deps, dataset, entry.recordId)) done.push({
865
+ action: 'delete',
866
+ recordId: entry.recordId
867
+ });
868
+ else conflicts.push(step);
869
+ continue;
870
+ }
871
+ // Restore (or a conflict the person chose to revert): the fields the
872
+ // import wrote, back to what they held before it.
873
+ const stored = _extends({}, (_doc_get = doc.get('values')) != null ? _doc_get : {});
874
+ const { values, cleared } = datasetStorageValues(model, step.values);
875
+ const merged = _extends({}, stored, values);
876
+ for (const fieldId of cleared)delete merged[fieldId];
877
+ restores.set(doc.ref, _extends({
878
+ values: merged
879
+ }, datasetIntegrityUpdate(model, merged, FieldValue.delete()), {
880
+ updatedAt: Timestamp.now()
881
+ }), {
882
+ mergeFields: RECORD_MERGE_FIELDS
883
+ });
884
+ restoring += 1;
885
+ done.push({
886
+ action: 'restore',
887
+ recordId: entry.recordId,
888
+ values: step.values
889
+ });
890
+ }
891
+ if (restoring) await restores.commit();
892
+ if (done.some((step)=>step.action !== 'nothing')) {
893
+ await announceDatasetRecords({
894
+ firestore: deps.firestore,
895
+ orgId: ctx.orgId,
896
+ datasetId: dataset.id
897
+ });
898
+ }
899
+ return {
900
+ done,
901
+ conflicts
902
+ };
903
+ }
904
+ /*------------------------------------------
905
+ * The hooks
906
+ *-----------------------------------------*/ /** The records hooks for `data.dataset`, over `deps` (the Admin SDK's Firestore by default). */ export function datasetTransferHooks(deps = productionDeps) {
907
+ return {
908
+ fields: async (ctx)=>{
909
+ const dataset = await datasetFor(deps(), ctx);
910
+ return datasetTransferCatalog(dataset.model, dataset.label, await datasetReferenceTargets(deps(), ctx, dataset.model));
911
+ },
912
+ matchKeys: async (ctx)=>datasetMatchKeyOffer((await datasetFor(deps(), ctx)).model),
913
+ count: (ctx, options)=>countDatasetRecords(deps(), ctx, options),
914
+ readPage: (ctx, cursor, fieldIds, options)=>readDatasetPage(deps(), ctx, cursor, fieldIds, options),
915
+ lookup: (ctx, requests)=>lookupDatasetRecords(deps(), ctx, requests),
916
+ suggest: (ctx, request)=>suggestDatasetRecords(deps(), ctx, request),
917
+ picklists: (ctx, picklistIds)=>datasetPicklists(deps(), ctx, picklistIds),
918
+ addPicklistValues: (ctx, picklistId, values)=>addDatasetOptions(deps(), ctx, picklistId, values),
919
+ plan: async (ctx, input)=>{
920
+ const dataset = await datasetFor(deps(), ctx);
921
+ await ensureDeclaredCustomFieldTypes(dataset.model);
922
+ const plan = refuseInvalidDatasetRows(dataset.model, buildTransferPlan(input), input.existing);
923
+ return limitDatasetCreates(plan, (await createLimits(dataset)).maxCreates);
924
+ },
925
+ apply: (ctx, chunk, writer)=>applyDatasetChunk(deps(), ctx, chunk, writer),
926
+ revert: (ctx, snapshot, decisions)=>revertDatasetChunk(deps(), ctx, snapshot, decisions)
927
+ };
928
+ }
929
+
930
+ //# sourceMappingURL=dataset-transfer.server.js.map