@aglyn/plugins-data 1.0.0-beta.217 → 1.0.0-beta.219

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 (30) hide show
  1. package/package.json +11 -9
  2. package/src/lib/constants/bundle-common.d.ts +6 -0
  3. package/src/lib/constants/bundle-common.js +5 -0
  4. package/src/lib/constants/bundle-common.js.map +1 -1
  5. package/src/lib/declarations.server.d.ts +39 -0
  6. package/src/lib/declarations.server.js +63 -0
  7. package/src/lib/declarations.server.js.map +1 -0
  8. package/src/lib/form-target/dataset-form-record-target.server.d.ts +40 -0
  9. package/src/lib/form-target/dataset-form-record-target.server.js +160 -0
  10. package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -0
  11. package/src/lib/form-target/form-dataset-binding-token.d.ts +35 -0
  12. package/src/lib/form-target/form-dataset-binding-token.js +107 -0
  13. package/src/lib/form-target/form-dataset-binding-token.js.map +1 -0
  14. package/src/lib/form-target/form-dataset-binding.d.ts +50 -0
  15. package/src/lib/form-target/form-dataset-binding.js +74 -0
  16. package/src/lib/form-target/form-dataset-binding.js.map +1 -0
  17. package/src/lib/form-target/stamp-form-dataset-bindings.d.ts +39 -0
  18. package/src/lib/form-target/stamp-form-dataset-bindings.js +65 -0
  19. package/src/lib/form-target/stamp-form-dataset-bindings.js.map +1 -0
  20. package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -2
  21. package/src/lib/repeat/dataset-repeat-rows.js +2 -2
  22. package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
  23. package/src/lib/repeat/dataset-repeat-rows.server.d.ts +43 -0
  24. package/src/lib/repeat/dataset-repeat-rows.server.js +170 -0
  25. package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -0
  26. package/src/lib/repeat/dataset-repeat-source.js +3 -1
  27. package/src/lib/repeat/dataset-repeat-source.js.map +1 -1
  28. package/src/lib/site-bundle/datasets-site-bundle.server.d.ts +87 -0
  29. package/src/lib/site-bundle/datasets-site-bundle.server.js +197 -0
  30. package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { SiteBundleImportRequest, SiteBundleItem, SiteBundleReportRow, SiteBundleRequest, SiteBundleRestoreRequest } from '@aglyn/aglyn/plugin-manager/plugin-site-bundle';
18
+ /**
19
+ * A site's datasets in its whole-site backup (AGL-163), answered by the plugin
20
+ * that keeps them (AGL-3080).
21
+ *
22
+ * Datasets are ORG-owned (AGL-237) and shared with sites by scope, so a site's
23
+ * backup carries the datasets that site may see (AGL-1046) — each with its
24
+ * records — and a restore writes them back into the owning organization,
25
+ * scoped to the site it restores into.
26
+ */
27
+ /** The records one dataset carries in a bundle, and the most a restore writes back. */
28
+ export declare const DATASET_BUNDLE_RECORD_LIMIT = 1000;
29
+ /**
30
+ * The keys a restore writes back on a dataset (AGL-1382): every key a live
31
+ * dataset carries, because the restore writes with `merge: false` and a key
32
+ * left off is ERASED. `visibleTo` is never on it — the restore assigns a fresh
33
+ * scope for the site it restores into, since a bundle is portable and an
34
+ * embedded `['org']` would publish one organization's data across another's
35
+ * whole client roster.
36
+ */
37
+ export declare const DATASET_RESTORE_FIELDS: readonly string[];
38
+ /**
39
+ * The keys a restore writes back on a record. `order` is what
40
+ * `sortDatasetRecords` sorts by, so dropping it reorders every repeatable on
41
+ * the restored site. The integrity index is DERIVED on the way in, never
42
+ * carried (see {@link importSiteDatasets}).
43
+ */
44
+ export declare const RECORD_RESTORE_FIELDS: readonly string[];
45
+ /**
46
+ * The site's share: the datasets the site may see, each with its records.
47
+ *
48
+ * A site with no organization genuinely has no datasets — a known-empty
49
+ * answer. Everything else THROWS: a catch-all would ship a silently empty,
50
+ * still-successful "backup" that nobody discovers until they restore it. An
51
+ * export that fails loudly is recoverable; one that lies is not.
52
+ *
53
+ * `visibleTo` is stripped on the way out. Its `host:` tokens name sites of
54
+ * THIS organization, so they are noise at best and a dangling reference once
55
+ * the bundle is restored elsewhere; the restore assigns a fresh scope.
56
+ */
57
+ export declare function exportSiteDatasets({ hostId, orgId, limit, }: SiteBundleRequest): Promise<SiteBundleItem[]>;
58
+ /**
59
+ * Refuse a bundle that would put the workspace over the datasets it may hold
60
+ * (AGL-1403) — decided before the restore writes anything.
61
+ *
62
+ * Datasets are SOLD: the plan includes some and the workspace buys more as
63
+ * add-ons, so `checkDatasetQuota` decides, never the plan's included number —
64
+ * a workspace that paid for extra datasets is entitled to them, and refusing
65
+ * its own backup after taking its money for the room would be wrong.
66
+ *
67
+ * A restore is identified by id COLLISION, never by the bundle's own claim of
68
+ * where it came from (an unsigned string in a file the metered party
69
+ * uploads). The meter is per organization, so a collision here means the
70
+ * WORKSPACE already holds the dataset: restoring into a sibling site of the
71
+ * same workspace rewrites datasets it already pays for and raises nothing,
72
+ * while a copy into a different workspace collides on nothing and is refused
73
+ * when it lands that workspace over. What is refused is the RAISE, never the
74
+ * state of being over — a workspace already over its limit may still restore
75
+ * its own backup.
76
+ */
77
+ export declare function siteDatasetsRefusal({ orgId, org, items, }: SiteBundleRestoreRequest): Promise<string | null>;
78
+ /**
79
+ * Writes the bundle's datasets into the owning organization, scoped to the
80
+ * site restored into (AGL-237, AGL-1046), each with its records.
81
+ *
82
+ * Non-conforming records are restored AND reported (AGL-182): data is never
83
+ * silently dropped, and the report tells the owner what to fix. A bundle from
84
+ * before typed models validates through the derived text model, as the live
85
+ * migration does, so everything in it passes.
86
+ */
87
+ export declare function importSiteDatasets(request: SiteBundleImportRequest): Promise<SiteBundleReportRow[]>;
@@ -0,0 +1,197 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ /**
4
+ * @license
5
+ * Copyright 2026 Aglyn LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */ import { datasetIntegrityFields, effectiveDatasetModel, validateDocument } from "@aglyn/aglyn/app-utils/dataset-models";
19
+ import { checkDatasetQuota } from "@aglyn/aglyn/app-utils/plan-entitlements";
20
+ import { hostScopeToken, newResourceScopeFields } from "@aglyn/aglyn/app-utils/scope-tokens";
21
+ import { firebaseAdmin, scopedToHost } from "@aglyn/tenant-data-admin";
22
+ /**
23
+ * A site's datasets in its whole-site backup (AGL-163), answered by the plugin
24
+ * that keeps them (AGL-3080).
25
+ *
26
+ * Datasets are ORG-owned (AGL-237) and shared with sites by scope, so a site's
27
+ * backup carries the datasets that site may see (AGL-1046) — each with its
28
+ * records — and a restore writes them back into the owning organization,
29
+ * scoped to the site it restores into.
30
+ */ /** The records one dataset carries in a bundle, and the most a restore writes back. */ export const DATASET_BUNDLE_RECORD_LIMIT = 1000;
31
+ /**
32
+ * The keys a restore writes back on a dataset (AGL-1382): every key a live
33
+ * dataset carries, because the restore writes with `merge: false` and a key
34
+ * left off is ERASED. `visibleTo` is never on it — the restore assigns a fresh
35
+ * scope for the site it restores into, since a bundle is portable and an
36
+ * embedded `['org']` would publish one organization's data across another's
37
+ * whole client roster.
38
+ */ export const DATASET_RESTORE_FIELDS = [
39
+ 'displayName',
40
+ // Pre-AGL-536 human name, still read as a fallback by site search.
41
+ 'name',
42
+ 'fields',
43
+ // The typed DatasetModel, and exactly what `effectiveDatasetModel` reads
44
+ // below. Drop it and every restored dataset silently degrades to the
45
+ // derived all-text v1 model, losing types, `required`, `validation`,
46
+ // `default` and every `reference` link (AGL-180).
47
+ 'model',
48
+ 'names',
49
+ 'description',
50
+ 'source',
51
+ 'installedFrom',
52
+ 'detachedFrom'
53
+ ];
54
+ /**
55
+ * The keys a restore writes back on a record. `order` is what
56
+ * `sortDatasetRecords` sorts by, so dropping it reorders every repeatable on
57
+ * the restored site. The integrity index is DERIVED on the way in, never
58
+ * carried (see {@link importSiteDatasets}).
59
+ */ export const RECORD_RESTORE_FIELDS = [
60
+ 'values',
61
+ 'order'
62
+ ];
63
+ /** One of a bundle's documents, reduced to the keys a restore may write. */ function restorable(fields, input) {
64
+ const allowed = new Set(fields);
65
+ const out = {};
66
+ for (const [key, value] of Object.entries(input != null ? input : {})){
67
+ // Only `undefined` is absence — Firestore rejects it outright.
68
+ if (allowed.has(key) && value !== undefined) out[key] = value;
69
+ }
70
+ return out;
71
+ }
72
+ /** The ids a bundle would write — distinct, since two items sharing one are one document. */ function bundleIds(items) {
73
+ const ids = new Set();
74
+ for (const item of items)if (item == null ? void 0 : item.$id) ids.add(String(item.$id));
75
+ return ids;
76
+ }
77
+ const datasetsOf = (orgId)=>firebaseAdmin.app().firestore().collection('orgs').doc(orgId).collection('datasets');
78
+ /**
79
+ * The site's share: the datasets the site may see, each with its records.
80
+ *
81
+ * A site with no organization genuinely has no datasets — a known-empty
82
+ * answer. Everything else THROWS: a catch-all would ship a silently empty,
83
+ * still-successful "backup" that nobody discovers until they restore it. An
84
+ * export that fails loudly is recoverable; one that lies is not.
85
+ *
86
+ * `visibleTo` is stripped on the way out. Its `host:` tokens name sites of
87
+ * THIS organization, so they are noise at best and a dangling reference once
88
+ * the bundle is restored elsewhere; the restore assigns a fresh scope.
89
+ */ export async function exportSiteDatasets({ hostId, orgId, limit }) {
90
+ if (!orgId) return [];
91
+ const snapshot = await scopedToHost(datasetsOf(orgId), hostId).limit(limit).get();
92
+ const live = snapshot.docs.filter((doc)=>!doc.get('deletedAt'));
93
+ return Promise.all(live.map(async (doc)=>{
94
+ const _doc_data = doc.data(), { visibleTo: _visibleTo } = _doc_data, data = _object_without_properties_loose(_doc_data, [
95
+ "visibleTo"
96
+ ]);
97
+ const records = await doc.ref.collection('records').limit(DATASET_BUNDLE_RECORD_LIMIT).get();
98
+ return _extends({
99
+ $id: doc.id
100
+ }, data, {
101
+ records: records.docs.map((record)=>_extends({
102
+ $id: record.id
103
+ }, record.data()))
104
+ });
105
+ }));
106
+ }
107
+ /**
108
+ * Refuse a bundle that would put the workspace over the datasets it may hold
109
+ * (AGL-1403) — decided before the restore writes anything.
110
+ *
111
+ * Datasets are SOLD: the plan includes some and the workspace buys more as
112
+ * add-ons, so `checkDatasetQuota` decides, never the plan's included number —
113
+ * a workspace that paid for extra datasets is entitled to them, and refusing
114
+ * its own backup after taking its money for the room would be wrong.
115
+ *
116
+ * A restore is identified by id COLLISION, never by the bundle's own claim of
117
+ * where it came from (an unsigned string in a file the metered party
118
+ * uploads). The meter is per organization, so a collision here means the
119
+ * WORKSPACE already holds the dataset: restoring into a sibling site of the
120
+ * same workspace rewrites datasets it already pays for and raises nothing,
121
+ * while a copy into a different workspace collides on nothing and is refused
122
+ * when it lands that workspace over. What is refused is the RAISE, never the
123
+ * state of being over — a workspace already over its limit may still restore
124
+ * its own backup.
125
+ */ export async function siteDatasetsRefusal({ orgId, org, items }) {
126
+ const bundled = bundleIds(items);
127
+ if (!orgId || !bundled.size) return null;
128
+ if (!Number.isFinite(checkDatasetQuota(org, 0).limit)) return null;
129
+ // A field mask with no fields projects to the document id alone.
130
+ const held = await datasetsOf(orgId).select().get();
131
+ const existing = new Set(held.docs.map((doc)=>doc.id));
132
+ const next = new Set([
133
+ ...existing,
134
+ ...bundled
135
+ ]).size;
136
+ // `allowed` asks "may I add one more to N", so the post state N is checked
137
+ // as `N - 1` — the idiom the dataset routes' own bulk paths use.
138
+ const quota = checkDatasetQuota(org, next - 1);
139
+ if (next <= existing.size || quota.allowed) return null;
140
+ return `This backup holds ${bundled.size} datasets and this workspace ` + `has ${existing.size}, which would put it at ${next} of ${quota.limit} ` + 'datasets. Nothing was imported — ' + (quota.upgradeRequired ? 'upgrade in Billing.' : `add extra datasets for $${quota.addonPriceUsd}/mo each, or ` + 'upgrade in Billing.');
141
+ }
142
+ /**
143
+ * Writes the bundle's datasets into the owning organization, scoped to the
144
+ * site restored into (AGL-237, AGL-1046), each with its records.
145
+ *
146
+ * Non-conforming records are restored AND reported (AGL-182): data is never
147
+ * silently dropped, and the report tells the owner what to fix. A bundle from
148
+ * before typed models validates through the derived text model, as the live
149
+ * migration does, so everything in it passes.
150
+ */ export async function importSiteDatasets(request) {
151
+ const { hostId, orgId, items, write, stamps } = request;
152
+ if (!orgId) return [];
153
+ // Through the scoped-create gate (AGL-1478, AGL-1484): a restore CREATES
154
+ // datasets, and a dataset is born scoped to the site it lands in.
155
+ const scope = newResourceScopeFields([
156
+ hostScopeToken(hostId)
157
+ ]);
158
+ const report = [];
159
+ for (const item of items){
160
+ if (!(item == null ? void 0 : item.$id)) continue;
161
+ const path = `orgs/${orgId}/datasets/${String(item.$id)}`;
162
+ await write(path, _extends({}, restorable(DATASET_RESTORE_FIELDS, item), stamps(), scope));
163
+ const model = effectiveDatasetModel(item);
164
+ await ensureCustomFieldTypes(model, request);
165
+ const records = Array.isArray(item['records']) ? item['records'] : [];
166
+ for (const record of records.slice(0, DATASET_BUNDLE_RECORD_LIMIT)){
167
+ var _record_values, _record_values1;
168
+ if (!(record == null ? void 0 : record.$id)) continue;
169
+ const errors = validateDocument(model, (_record_values = record['values']) != null ? _record_values : {});
170
+ if (Object.keys(errors).length) {
171
+ report.push({
172
+ datasetId: String(item.$id),
173
+ recordId: String(record.$id),
174
+ errors
175
+ });
176
+ }
177
+ await write(`${path}/records/${String(record.$id)}`, _extends({}, restorable(RECORD_RESTORE_FIELDS, record), stamps(), datasetIntegrityFields(model, (_record_values1 = record['values']) != null ? _record_values1 : {})));
178
+ }
179
+ }
180
+ return report;
181
+ }
182
+ /**
183
+ * Loads the plugins that register custom field types before a record is
184
+ * validated against `model` (AGL-434). A type is registered when its plugin's
185
+ * server surface loads, and `validateDocument` answers "no error" for a type
186
+ * nobody registered — right for a plugin that is absent, wrong for one not yet
187
+ * loaded. Only a model that names a custom type pays for the load.
188
+ */ async function ensureCustomFieldTypes(model, request) {
189
+ var _ref;
190
+ const usesCustomType = ((_ref = model == null ? void 0 : model.order) != null ? _ref : []).some((fieldId)=>{
191
+ var _model_fields_fieldId, _model_fields;
192
+ return Boolean(model == null ? void 0 : (_model_fields = model.fields) == null ? void 0 : (_model_fields_fieldId = _model_fields[fieldId]) == null ? void 0 : _model_fields_fieldId.customType);
193
+ });
194
+ if (usesCustomType) await request.loadPluginSurfaces();
195
+ }
196
+
197
+ //# sourceMappingURL=datasets-site-bundle.server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/site-bundle/datasets-site-bundle.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n datasetIntegrityFields,\n effectiveDatasetModel,\n validateDocument,\n} from '@aglyn/aglyn/app-utils/dataset-models'\nimport { checkDatasetQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { hostScopeToken, newResourceScopeFields } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type {\n SiteBundleImportRequest,\n SiteBundleItem,\n SiteBundleReportRow,\n SiteBundleRequest,\n SiteBundleRestoreRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-site-bundle'\nimport { firebaseAdmin, scopedToHost } from '@aglyn/tenant-data-admin'\n\n/**\n * A site's datasets in its whole-site backup (AGL-163), answered by the plugin\n * that keeps them (AGL-3080).\n *\n * Datasets are ORG-owned (AGL-237) and shared with sites by scope, so a site's\n * backup carries the datasets that site may see (AGL-1046) — each with its\n * records — and a restore writes them back into the owning organization,\n * scoped to the site it restores into.\n */\n\n/** The records one dataset carries in a bundle, and the most a restore writes back. */\nexport const DATASET_BUNDLE_RECORD_LIMIT = 1000\n\n/**\n * The keys a restore writes back on a dataset (AGL-1382): every key a live\n * dataset carries, because the restore writes with `merge: false` and a key\n * left off is ERASED. `visibleTo` is never on it — the restore assigns a fresh\n * scope for the site it restores into, since a bundle is portable and an\n * embedded `['org']` would publish one organization's data across another's\n * whole client roster.\n */\nexport const DATASET_RESTORE_FIELDS: readonly string[] = [\n 'displayName',\n // Pre-AGL-536 human name, still read as a fallback by site search.\n 'name',\n 'fields',\n // The typed DatasetModel, and exactly what `effectiveDatasetModel` reads\n // below. Drop it and every restored dataset silently degrades to the\n // derived all-text v1 model, losing types, `required`, `validation`,\n // `default` and every `reference` link (AGL-180).\n 'model',\n 'names',\n 'description',\n 'source',\n 'installedFrom',\n 'detachedFrom',\n]\n\n/**\n * The keys a restore writes back on a record. `order` is what\n * `sortDatasetRecords` sorts by, so dropping it reorders every repeatable on\n * the restored site. The integrity index is DERIVED on the way in, never\n * carried (see {@link importSiteDatasets}).\n */\nexport const RECORD_RESTORE_FIELDS: readonly string[] = ['values', 'order']\n\n/** One of a bundle's documents, reduced to the keys a restore may write. */\nfunction restorable(fields: readonly string[], input: SiteBundleItem): Record<string, unknown> {\n const allowed = new Set(fields)\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(input ?? {})) {\n // Only `undefined` is absence — Firestore rejects it outright.\n if (allowed.has(key) && value !== undefined) out[key] = value\n }\n return out\n}\n\n/** The ids a bundle would write — distinct, since two items sharing one are one document. */\nfunction bundleIds(items: readonly SiteBundleItem[]): Set<string> {\n const ids = new Set<string>()\n for (const item of items) if (item?.$id) ids.add(String(item.$id))\n return ids\n}\n\nconst datasetsOf = (orgId: string) =>\n firebaseAdmin.app().firestore().collection('orgs').doc(orgId).collection('datasets')\n\n/**\n * The site's share: the datasets the site may see, each with its records.\n *\n * A site with no organization genuinely has no datasets — a known-empty\n * answer. Everything else THROWS: a catch-all would ship a silently empty,\n * still-successful \"backup\" that nobody discovers until they restore it. An\n * export that fails loudly is recoverable; one that lies is not.\n *\n * `visibleTo` is stripped on the way out. Its `host:` tokens name sites of\n * THIS organization, so they are noise at best and a dangling reference once\n * the bundle is restored elsewhere; the restore assigns a fresh scope.\n */\nexport async function exportSiteDatasets({\n hostId,\n orgId,\n limit,\n}: SiteBundleRequest): Promise<SiteBundleItem[]> {\n if (!orgId) return []\n const snapshot = await scopedToHost(datasetsOf(orgId), hostId).limit(limit).get()\n const live = snapshot.docs.filter((doc) => !doc.get('deletedAt'))\n return Promise.all(\n live.map(async (doc) => {\n const { visibleTo: _visibleTo, ...data } = doc.data()\n const records = await doc.ref.collection('records').limit(DATASET_BUNDLE_RECORD_LIMIT).get()\n return {\n $id: doc.id,\n ...data,\n records: records.docs.map((record) => ({ $id: record.id, ...record.data() })),\n }\n }),\n )\n}\n\n/**\n * Refuse a bundle that would put the workspace over the datasets it may hold\n * (AGL-1403) — decided before the restore writes anything.\n *\n * Datasets are SOLD: the plan includes some and the workspace buys more as\n * add-ons, so `checkDatasetQuota` decides, never the plan's included number —\n * a workspace that paid for extra datasets is entitled to them, and refusing\n * its own backup after taking its money for the room would be wrong.\n *\n * A restore is identified by id COLLISION, never by the bundle's own claim of\n * where it came from (an unsigned string in a file the metered party\n * uploads). The meter is per organization, so a collision here means the\n * WORKSPACE already holds the dataset: restoring into a sibling site of the\n * same workspace rewrites datasets it already pays for and raises nothing,\n * while a copy into a different workspace collides on nothing and is refused\n * when it lands that workspace over. What is refused is the RAISE, never the\n * state of being over — a workspace already over its limit may still restore\n * its own backup.\n */\nexport async function siteDatasetsRefusal({\n orgId,\n org,\n items,\n}: SiteBundleRestoreRequest): Promise<string | null> {\n const bundled = bundleIds(items)\n if (!orgId || !bundled.size) return null\n if (!Number.isFinite(checkDatasetQuota(org as any, 0).limit)) return null\n\n // A field mask with no fields projects to the document id alone.\n const held = await datasetsOf(orgId).select().get()\n const existing = new Set(held.docs.map((doc) => doc.id))\n const next = new Set([...existing, ...bundled]).size\n // `allowed` asks \"may I add one more to N\", so the post state N is checked\n // as `N - 1` — the idiom the dataset routes' own bulk paths use.\n const quota = checkDatasetQuota(org as any, next - 1)\n if (next <= existing.size || quota.allowed) return null\n\n return (\n `This backup holds ${bundled.size} datasets and this workspace ` +\n `has ${existing.size}, which would put it at ${next} of ${quota.limit} ` +\n 'datasets. Nothing was imported — ' +\n (quota.upgradeRequired\n ? 'upgrade in Billing.'\n : `add extra datasets for $${quota.addonPriceUsd}/mo each, or ` +\n 'upgrade in Billing.')\n )\n}\n\n/**\n * Writes the bundle's datasets into the owning organization, scoped to the\n * site restored into (AGL-237, AGL-1046), each with its records.\n *\n * Non-conforming records are restored AND reported (AGL-182): data is never\n * silently dropped, and the report tells the owner what to fix. A bundle from\n * before typed models validates through the derived text model, as the live\n * migration does, so everything in it passes.\n */\nexport async function importSiteDatasets(\n request: SiteBundleImportRequest,\n): Promise<SiteBundleReportRow[]> {\n const { hostId, orgId, items, write, stamps } = request\n if (!orgId) return []\n // Through the scoped-create gate (AGL-1478, AGL-1484): a restore CREATES\n // datasets, and a dataset is born scoped to the site it lands in.\n const scope = newResourceScopeFields([hostScopeToken(hostId)])\n const report: SiteBundleReportRow[] = []\n for (const item of items) {\n if (!item?.$id) continue\n const path = `orgs/${orgId}/datasets/${String(item.$id)}`\n await write(path, { ...restorable(DATASET_RESTORE_FIELDS, item), ...stamps(), ...scope })\n const model = effectiveDatasetModel(item)\n await ensureCustomFieldTypes(model, request)\n const records: SiteBundleItem[] = Array.isArray(item['records']) ? item['records'] : []\n for (const record of records.slice(0, DATASET_BUNDLE_RECORD_LIMIT)) {\n if (!record?.$id) continue\n const errors = validateDocument(model, record['values'] ?? {})\n if (Object.keys(errors).length) {\n report.push({ datasetId: String(item.$id), recordId: String(record.$id), errors })\n }\n await write(`${path}/records/${String(record.$id)}`, {\n ...restorable(RECORD_RESTORE_FIELDS, record),\n ...stamps(),\n // DERIVED here rather than carried in the bundle. The integrity\n // index the delete check queries must describe the values just\n // restored, and a bundle written before the field existed carries\n // none — accepting the bundle's copy would restore records the check\n // cannot reach. Records keep their original ids, so the ids it holds\n // still resolve.\n ...datasetIntegrityFields(model, record['values'] ?? {}),\n })\n }\n }\n return report\n}\n\n/**\n * Loads the plugins that register custom field types before a record is\n * validated against `model` (AGL-434). A type is registered when its plugin's\n * server surface loads, and `validateDocument` answers \"no error\" for a type\n * nobody registered — right for a plugin that is absent, wrong for one not yet\n * loaded. Only a model that names a custom type pays for the load.\n */\nasync function ensureCustomFieldTypes(\n model: ReturnType<typeof effectiveDatasetModel>,\n request: Pick<SiteBundleImportRequest, 'loadPluginSurfaces'>,\n): Promise<void> {\n const usesCustomType = (model?.order ?? []).some((fieldId) =>\n Boolean(model?.fields?.[fieldId]?.customType),\n )\n if (usesCustomType) await request.loadPluginSurfaces()\n}\n"],"names":["datasetIntegrityFields","effectiveDatasetModel","validateDocument","checkDatasetQuota","hostScopeToken","newResourceScopeFields","firebaseAdmin","scopedToHost","DATASET_BUNDLE_RECORD_LIMIT","DATASET_RESTORE_FIELDS","RECORD_RESTORE_FIELDS","restorable","fields","input","allowed","Set","out","key","value","Object","entries","has","undefined","bundleIds","items","ids","item","$id","add","String","datasetsOf","orgId","app","firestore","collection","doc","exportSiteDatasets","hostId","limit","snapshot","get","live","docs","filter","Promise","all","map","data","visibleTo","_visibleTo","records","ref","id","record","siteDatasetsRefusal","org","bundled","size","Number","isFinite","held","select","existing","next","quota","upgradeRequired","addonPriceUsd","importSiteDatasets","request","write","stamps","scope","report","path","model","ensureCustomFieldTypes","Array","isArray","slice","errors","keys","length","push","datasetId","recordId","usesCustomType","order","some","fieldId","Boolean","customType","loadPluginSurfaces"],"mappings":"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"}