@aglyn/plugins-data 1.0.0-beta.233 → 1.0.0-beta.235
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.
- package/package.json +10 -10
- package/src/lib/components/dataset-record-filter.d.ts +10 -1
- package/src/lib/components/dataset-record-filter.js +12 -1
- package/src/lib/components/dataset-record-filter.js.map +1 -1
- package/src/lib/components/dataset-schema-dialog.component.js +2 -11
- package/src/lib/components/dataset-schema-dialog.component.js.map +1 -1
- package/src/lib/components/host-datasets-card.component.js +19 -32
- package/src/lib/components/host-datasets-card.component.js.map +1 -1
- package/src/lib/model/dataset-model-core.d.ts +299 -0
- package/src/lib/model/dataset-model-core.js +511 -0
- package/src/lib/model/dataset-model-core.js.map +1 -0
- package/src/lib/model/dataset-models.d.ts +2 -231
- package/src/lib/model/dataset-models.js +16 -392
- package/src/lib/model/dataset-models.js.map +1 -1
- package/src/lib/model/dataset-record-delete-plan.d.ts +60 -0
- package/src/lib/model/dataset-record-delete-plan.js +82 -0
- package/src/lib/model/dataset-record-delete-plan.js.map +1 -0
- package/src/lib/model/datasets.d.ts +2 -39
- package/src/lib/model/datasets.js +5 -77
- package/src/lib/model/datasets.js.map +1 -1
- package/src/lib/server/datasets-route.d.ts +3 -0
- package/src/lib/server/datasets-route.js +146 -3
- package/src/lib/server/datasets-route.js.map +1 -1
|
@@ -30,6 +30,9 @@ import { type PluginWebApiHandler } from '@aglyn/aglyn/server';
|
|
|
30
30
|
* - `import-records`: batch create with the whole batch fitting the cap.
|
|
31
31
|
* - `add-address-field`: a "Page address" field that fills in from a text
|
|
32
32
|
* field, and a unique address on every record that has none (AGL-3475).
|
|
33
|
+
* - `update-record` / `delete-record`: the Data card's record edit and
|
|
34
|
+
* delete (with its reference integrity) for the native apps, which have no
|
|
35
|
+
* browser to write from (AGL-3668).
|
|
33
36
|
*
|
|
34
37
|
* Since AGL-2163 both record actions ALSO enforce `checkDataStorageQuota`.
|
|
35
38
|
* See {@link refuseIfDataStorageBlocked}: that check's `allowed` field had no
|
|
@@ -22,6 +22,7 @@ import { FieldPath, FieldValue, Timestamp } from "firebase-admin/firestore";
|
|
|
22
22
|
import { announceDatasetRecords as announceDatasetChange } from "./announce-dataset-records.js";
|
|
23
23
|
import { coerceDocumentValues, datasetIntegrityFields, datasetIntegrityUpdate, effectiveDatasetModel, validateDocument } from "../model/dataset-models.js";
|
|
24
24
|
import { defaultDatasetFieldId } from "../model/datasets.js";
|
|
25
|
+
import { planReferenceFixups, referencingFieldIds, restrictedDeleteMessage } from "../model/dataset-record-delete-plan.js";
|
|
25
26
|
import { RECORD_PAGE_ADDRESS_FIELD_TYPE, fillRecordAddresses, isRecordAddressField, recordAddressFieldIds, recordAddressOf, uniqueRecordAddresses } from "../record-pages/record-pages.js";
|
|
26
27
|
/**
|
|
27
28
|
* `dataStorageMbPerOrg` for this route, rendered as the console's 403.
|
|
@@ -71,6 +72,9 @@ import { RECORD_PAGE_ADDRESS_FIELD_TYPE, fillRecordAddresses, isRecordAddressFie
|
|
|
71
72
|
* - `import-records`: batch create with the whole batch fitting the cap.
|
|
72
73
|
* - `add-address-field`: a "Page address" field that fills in from a text
|
|
73
74
|
* field, and a unique address on every record that has none (AGL-3475).
|
|
75
|
+
* - `update-record` / `delete-record`: the Data card's record edit and
|
|
76
|
+
* delete (with its reference integrity) for the native apps, which have no
|
|
77
|
+
* browser to write from (AGL-3668).
|
|
74
78
|
*
|
|
75
79
|
* Since AGL-2163 both record actions ALSO enforce `checkDataStorageQuota`.
|
|
76
80
|
* See {@link refuseIfDataStorageBlocked}: that check's `allowed` field had no
|
|
@@ -307,7 +311,7 @@ import { RECORD_PAGE_ADDRESS_FIELD_TYPE, fillRecordAddresses, isRecordAddressFie
|
|
|
307
311
|
status: 200
|
|
308
312
|
});
|
|
309
313
|
}
|
|
310
|
-
if (action === 'create-record' || action === 'import-records' || action === 'announce-records' || action === 'add-address-field') {
|
|
314
|
+
if (action === 'create-record' || action === 'import-records' || action === 'announce-records' || action === 'add-address-field' || action === 'update-record' || action === 'delete-record') {
|
|
311
315
|
var _ref4;
|
|
312
316
|
var _datasetSnapshot_data;
|
|
313
317
|
const datasetId = String((_ref4 = body == null ? void 0 : body.datasetId) != null ? _ref4 : '');
|
|
@@ -466,11 +470,150 @@ import { RECORD_PAGE_ADDRESS_FIELD_TYPE, fillRecordAddresses, isRecordAddressFie
|
|
|
466
470
|
// once its plugin's server entry has registered it.
|
|
467
471
|
await ensureDeclaredCustomFieldTypes(model);
|
|
468
472
|
const recordsRef = datasetRef.collection('records');
|
|
473
|
+
/**
|
|
474
|
+
* A record edit (AGL-3668): the Data card's Save on an existing record,
|
|
475
|
+
* for a caller that is not a browser. The card writes it straight to
|
|
476
|
+
* Firestore — it consumes no quota — with the values coerced, filled
|
|
477
|
+
* and validated against the model and the integrity and filter index
|
|
478
|
+
* moved with them; the native apps reach the same write here, under
|
|
479
|
+
* the same role, `data.manage` and visibility gates as a create.
|
|
480
|
+
* `values` is replaced whole, so a removed field's value strips here,
|
|
481
|
+
* as it does on the card (the AGL-178 policy).
|
|
482
|
+
*/ if (action === 'update-record') {
|
|
483
|
+
var _ref6, _ref7;
|
|
484
|
+
const recordId = String((_ref6 = body == null ? void 0 : body.recordId) != null ? _ref6 : '');
|
|
485
|
+
if (!recordId) {
|
|
486
|
+
return Response.json({
|
|
487
|
+
error: 'Missing recordId'
|
|
488
|
+
}, {
|
|
489
|
+
status: 400
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
const recordRef = recordsRef.doc(recordId);
|
|
493
|
+
if (!(await recordRef.get()).exists) {
|
|
494
|
+
return Response.json({
|
|
495
|
+
error: 'Unknown record'
|
|
496
|
+
}, {
|
|
497
|
+
status: 404
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
const coerced = fillRecordAddresses(model, coerceDocumentValues(model, (_ref7 = body == null ? void 0 : body.values) != null ? _ref7 : {}));
|
|
501
|
+
const errors = validateDocument(model, coerced);
|
|
502
|
+
if (Object.keys(errors).length) {
|
|
503
|
+
return Response.json({
|
|
504
|
+
error: 'Record failed validation',
|
|
505
|
+
errors
|
|
506
|
+
}, {
|
|
507
|
+
status: 400
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
await recordRef.update(_extends({
|
|
511
|
+
values: coerced
|
|
512
|
+
}, datasetIntegrityUpdate(model, coerced, FieldValue.delete()), {
|
|
513
|
+
updatedAt: Timestamp.now()
|
|
514
|
+
}));
|
|
515
|
+
const announced = await announceDatasetChange({
|
|
516
|
+
firestore,
|
|
517
|
+
orgId,
|
|
518
|
+
datasetId
|
|
519
|
+
});
|
|
520
|
+
return Response.json({
|
|
521
|
+
ok: true,
|
|
522
|
+
id: recordId,
|
|
523
|
+
announced
|
|
524
|
+
}, {
|
|
525
|
+
status: 200
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* A record delete (AGL-3668), with the card's delete integrity
|
|
530
|
+
* (AGL-180): every dataset the caller can see whose model references
|
|
531
|
+
* this one is asked, by its `referencedIds` index, whether it still
|
|
532
|
+
* points at the record. `restrict` refuses the delete and writes
|
|
533
|
+
* nothing; `setNull` strips the reference from each holder first. The
|
|
534
|
+
* verdict is `planReferenceFixups`, the one the card applies. The
|
|
535
|
+
* datasets consulted are the ones the caller can see, exactly the set
|
|
536
|
+
* the card reads, so the app is refused exactly when the console is.
|
|
537
|
+
*/ if (action === 'delete-record') {
|
|
538
|
+
var _ref8;
|
|
539
|
+
const recordId = String((_ref8 = body == null ? void 0 : body.recordId) != null ? _ref8 : '');
|
|
540
|
+
if (!recordId) {
|
|
541
|
+
return Response.json({
|
|
542
|
+
error: 'Missing recordId'
|
|
543
|
+
}, {
|
|
544
|
+
status: 400
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
const recordRef = recordsRef.doc(recordId);
|
|
548
|
+
if (!(await recordRef.get()).exists) {
|
|
549
|
+
return Response.json({
|
|
550
|
+
error: 'Unknown record'
|
|
551
|
+
}, {
|
|
552
|
+
status: 404
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
const others = (await orgRef.collection('datasets').get()).docs.filter((other)=>decoded['staff'] === true || memberCanSee(member, other.data().visibleTo));
|
|
556
|
+
const fixups = [];
|
|
557
|
+
for (const other of others){
|
|
558
|
+
const otherModel = effectiveDatasetModel(other.data());
|
|
559
|
+
if (!referencingFieldIds(otherModel, datasetId).length) continue;
|
|
560
|
+
const holders = await other.ref.collection('records').where('referencedIds', 'array-contains', recordId).get();
|
|
561
|
+
const plan = planReferenceFixups(otherModel, datasetId, recordId, holders.docs.map((holder)=>({
|
|
562
|
+
id: holder.id,
|
|
563
|
+
values: holder.get('values')
|
|
564
|
+
})));
|
|
565
|
+
if (plan.kind === 'restricted') {
|
|
566
|
+
var _ref9, _ref10;
|
|
567
|
+
var _other_data, _other_data1;
|
|
568
|
+
const name = String((_ref9 = (_ref10 = (_other_data = other.data()) == null ? void 0 : _other_data.displayName) != null ? _ref10 : (_other_data1 = other.data()) == null ? void 0 : _other_data1.name) != null ? _ref9 : '').trim();
|
|
569
|
+
return Response.json({
|
|
570
|
+
error: restrictedDeleteMessage(plan.holders, name)
|
|
571
|
+
}, {
|
|
572
|
+
status: 409
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
if (plan.kind === 'strip') fixups.push({
|
|
576
|
+
datasetId: other.id,
|
|
577
|
+
model: otherModel,
|
|
578
|
+
updates: plan.updates
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
for (const fixup of fixups){
|
|
582
|
+
const holdersRef = orgRef.collection('datasets').doc(fixup.datasetId).collection('records');
|
|
583
|
+
for(let start = 0; start < fixup.updates.length; start += IMPORT_CHUNK){
|
|
584
|
+
const batch = firestore.batch();
|
|
585
|
+
for (const update of fixup.updates.slice(start, start + IMPORT_CHUNK)){
|
|
586
|
+
batch.update(holdersRef.doc(update.id), _extends({
|
|
587
|
+
values: update.values
|
|
588
|
+
}, datasetIntegrityUpdate(fixup.model, update.values, FieldValue.delete())));
|
|
589
|
+
}
|
|
590
|
+
await batch.commit();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
await recordRef.delete();
|
|
594
|
+
for (const changed of [
|
|
595
|
+
datasetId,
|
|
596
|
+
...fixups.map((fixup)=>fixup.datasetId)
|
|
597
|
+
]){
|
|
598
|
+
await announceDatasetChange({
|
|
599
|
+
firestore,
|
|
600
|
+
orgId,
|
|
601
|
+
datasetId: changed
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
return Response.json({
|
|
605
|
+
ok: true,
|
|
606
|
+
id: recordId,
|
|
607
|
+
fixed: fixups.reduce((total, fixup)=>total + fixup.updates.length, 0)
|
|
608
|
+
}, {
|
|
609
|
+
status: 200
|
|
610
|
+
});
|
|
611
|
+
}
|
|
469
612
|
const recordCount = (await recordsRef.count().get()).data().count;
|
|
470
613
|
const overRecordQuota = (limit)=>`Record limit reached (${limit}) — upgrade in Billing`;
|
|
471
614
|
if (action === 'create-record') {
|
|
472
|
-
var
|
|
473
|
-
const coerced = fillRecordAddresses(model, coerceDocumentValues(model, (
|
|
615
|
+
var _ref11;
|
|
616
|
+
const coerced = fillRecordAddresses(model, coerceDocumentValues(model, (_ref11 = body == null ? void 0 : body.values) != null ? _ref11 : {}));
|
|
474
617
|
const errors = validateDocument(model, coerced);
|
|
475
618
|
if (Object.keys(errors).length) {
|
|
476
619
|
return Response.json({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/datasets-route.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 defaultScopeForNewResource,\n newResourceScopeFields,\n pluginRequestFromWeb,\n type PluginWebApiHandler,\n} from '@aglyn/aglyn/server'\nimport {\n checkDatasetQuota,\n checkEntitlement,\n checkQuota,\n createResourceUid,\n ensureDeclaredCustomFieldTypes,\n memberCanSee,\n} from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n dataStorageRefusal,\n isImpersonationSession,\n isServerReleaseFlagOnForOrg,\n lockdownRefusal,\n memberHasOrgPermission,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { FieldPath, FieldValue, Timestamp } from 'firebase-admin/firestore'\nimport { announceDatasetRecords as announceDatasetChange } from './announce-dataset-records'\nimport { coerceDocumentValues, datasetIntegrityFields, datasetIntegrityUpdate, effectiveDatasetModel, validateDocument } from '../model/dataset-models'\nimport { defaultDatasetFieldId } from '../model/datasets'\nimport {\n RECORD_PAGE_ADDRESS_FIELD_TYPE,\n fillRecordAddresses,\n isRecordAddressField,\n recordAddressFieldIds,\n recordAddressOf,\n uniqueRecordAddresses,\n} from '../record-pages/record-pages'\n\n/**\n * `dataStorageMbPerOrg` for this route, rendered as the console's 403.\n *\n * The VERDICT moved to `utils/server/data-storage-gate` (AGL-2253) — it was\n * defined here, so `/v1` and the tenant form path, which write dataset bytes\n * through their own code, enforced nothing. Only the console wording is left\n * behind, because the other two callers answer different shapes.\n */\nasync function refuseIfDataStorageBlocked(\n org: unknown,\n orgRef: FirebaseFirestore.DocumentReference,\n): Promise<Response | null> {\n const refusal = await dataStorageRefusal(org, orgRef)\n if (!refusal) return null\n return Response.json(\n {\n error:\n refusal.basis === 'always'\n ? 'Dataset storage is not included on this plan — upgrade in Billing'\n : `Dataset storage limit reached (${refusal.includedMb} MB) — ` +\n 'upgrade in Billing',\n },\n { status: 403 },\n )\n}\n\n/** Roles allowed to create org data — mirrors rules' canWriteOrgData(). */\nconst WRITER_ROLES = new Set(['owner', 'admin', 'editor'])\n\n/**\n * Rows written per import transaction.\n *\n * Firestore allows at most 500 writes in one transaction, and this is the\n * chunk size the route already used for its batches, so nothing about the\n * shape of an import changed when the batches became transactions (AGL-2371).\n */\nconst IMPORT_CHUNK = 400\n\n/**\n * How many records one \"give it page addresses\" call fills (AGL-3475). Far\n * past a services or locations dataset; a larger one reports `truncated`, and\n * the next call fills the next records, since a filled record is skipped.\n */\nconst ADDRESS_FILL_MAX = 5000\n\n/**\n * Dataset/record creation API (AGL-473): creates moved out of the client\n * SDK so quotas and entitlements are enforced server-side — Firestore\n * rules deny client-side `create` on `orgs/{orgId}/datasets/**` (updates\n * and deletes stay client-direct, they don't consume quota). Actions:\n *\n * - `create-dataset`: `dataStore` entitlement + `checkDatasetQuota`\n * (addon-aware, org-scoped). An optional `hostId` names the site the\n * dataset is created from, which the org's Default sharing can narrow it\n * to.\n * - `create-record`: `recordsPerDataset` quota; values are re-coerced\n * and re-validated against the dataset's model server-side.\n * - `import-records`: batch create with the whole batch fitting the cap.\n * - `add-address-field`: a \"Page address\" field that fills in from a text\n * field, and a unique address on every record that has none (AGL-3475).\n *\n * Since AGL-2163 both record actions ALSO enforce `checkDataStorageQuota`.\n * See {@link refuseIfDataStorageBlocked}: that check's `allowed` field had no\n * reader anywhere in the platform while its docblock said free \"hard-blocks\n * at the included size\", so the byte band was documentation. `recordsPerDataset`\n * counts ROWS and does not bound their size, so it was never the same limit.\n */\nexport const datasetsHandler: PluginWebApiHandler = async (request) => {\n const { method, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n\n const orgId = String(body?.orgId ?? '')\n const action = String(body?.action ?? '')\n if (!orgId) return Response.json({ error: 'Missing orgId' }, { status: 400 })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const member = membership?.member as any\n if (!member || !WRITER_ROLES.has(String(member.role))) {\n return Response.json({ error: 'Editing org data requires the editor role' }, { status: 403 })\n }\n /*==========================================\n * AND the granular permission (AGL-2444).\n *\n * `data.manage` had ZERO consumers: an owner could build a custom role\n * with \"Manage data\" unticked, assign it, and the member kept creating,\n * editing and deleting datasets — because this gate read the raw role,\n * exactly the defect AGL-2350 fixed one route over on `hosts.create`.\n *\n * Identical for the built-in roles — `data.manage` is on for owner,\n * admin and editor, which is `WRITER_ROLES` — so nobody's access moves.\n * What it adds is that a custom role or a per-member override is now\n * honoured, which is the narrowing the docs sell.\n *\n * The role check is kept ALONGSIDE it rather than replaced. It mirrors\n * the rules' `canWriteOrgData()`, and a route that disagreed with the\n * rules would let one door open while the other stayed shut.\n *=========================================*/\n if (\n decoded['staff'] !== true &&\n !(await memberHasOrgPermission(orgId, member, 'data.manage'))\n ) {\n return Response.json({\n error: 'Your organization role does not allow editing organization data',\n }, { status: 403 })\n }\n\n // Release gate (AGL-1653). `<FeatureGate flag=\"release_data_store\">` on\n // the org Data page was the only gate, so org-level dataset\n // administration kept working with the flag off — the milder sibling of\n // the add-on store leak above it in the same issue. Same staff bypass as\n // `FeatureGate`'s `visible`, same 404: released-off means absent.\n if (\n decoded['staff'] !== true &&\n !(await isServerReleaseFlagOnForOrg('release_data_store', orgId))\n ) {\n return Response.json({ error: 'Not available' }, { status: 404 })\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return Response.json({ error: 'Unknown organization' }, { status: 404 })\n }\n const org = orgSnapshot.data() as any\n\n // Lockdown verdict (AGL-1506): subsumes the bare `orgSuspended`\n // projection check this route used to make — the org doc is read just\n // above anyway, so the full verdict (platform/org/user scopes, staff\n // bypass, distinct 423 body) costs no extra org read here.\n const locked = await lockdownRefusal({\n request,\n staff: decoded['staff'] === true,\n uid: decoded.uid,\n org,\n })\n if (locked) return locked\n\n if (action === 'create-dataset') {\n if (!checkEntitlement(org, 'dataStore')) {\n return Response.json({\n error: 'Datasets require a Starter plan or higher',\n }, { status: 403 })\n }\n const displayName = String(body?.displayName ?? '').trim().slice(0, 120)\n const fields = Array.isArray(body?.fields)\n ? (body.fields as unknown[]).map((field) => String(field)).slice(0, 100)\n : []\n if (!displayName || fields.length === 0) {\n return Response.json({ error: 'Missing displayName or fields' }, { status: 400 })\n }\n /**\n * The site the dataset is created from — null on the organization Data\n * page, which has none.\n *\n * It reaches the document only through the org's Default sharing: with\n * `defaultResourceScope: 'host'` the new dataset is visible to this site\n * alone. That makes it a client claim deciding who can see the result,\n * so it is honored only when it holds on both counts:\n *\n * * The site is one of THIS org's. `hostIndex` is the mirror the Data\n * card resolved `orgId` from in the first place, so a create made\n * from a real site page cannot disagree with it. Another org's site\n * would stamp a token that no site and no scoped member of this org\n * holds: a dataset only the org's admins could ever find.\n * * The caller's own access reaches the scope it produces. A scoped\n * member naming a site outside their access would create a dataset\n * they cannot see afterwards, which is the create the rules'\n * `canCreateScoped()` refuses on the collections clients create.\n *\n * Both are refused rather than widened to All sites. Widening is the\n * direction that shows a dataset to sites nobody chose.\n */\n const hostId = String(body?.hostId ?? '').trim() || null\n if (\n hostId &&\n (hostId.includes('/') || (await resolveOrgIdForHost(hostId)) !== orgId)\n ) {\n return Response.json(\n { error: 'That site is not one of this organization’s' },\n { status: 400 },\n )\n }\n // Honors the org's `defaultResourceScope` (AGL-1048), falling back to\n // org-wide — which is also the only answer with no site in context.\n const visibleTo = defaultScopeForNewResource({\n defaultResourceScope: (org as {\n defaultResourceScope?: 'org' | 'host'\n })?.defaultResourceScope,\n hostId,\n })\n if (\n hostId &&\n decoded['staff'] !== true &&\n !memberCanSee(member, visibleTo)\n ) {\n return Response.json(\n { error: 'Your access to this organization does not reach that site' },\n { status: 403 },\n )\n }\n const datasetsRef = orgRef.collection('datasets')\n const overDatasetQuota = (quota: ReturnType<typeof checkDatasetQuota>) =>\n quota.upgradeRequired\n ? `Dataset limit reached (${quota.limit}) — upgrade in Billing`\n : `Dataset limit reached (${quota.limit}) — add extra datasets ` +\n `for $${quota.addonPriceUsd}/mo each or upgrade in Billing`\n // The fast refusal. NOT the enforcement point — that is the transaction\n // below — but it answers a caller who is simply over their limit before\n // the request builds anything.\n const quota = checkDatasetQuota(\n org,\n (await datasetsRef.count().get()).data().count,\n )\n if (!quota.allowed) {\n return Response.json({ error: overDatasetQuota(quota) }, { status: 403 })\n }\n const id = createResourceUid()\n // The model rides from the console (deriveModelFromFields output or\n // the join-collection template). `model` is independent of `fields`,\n // so cap its serialized size explicitly (a legit model is well under\n // this — 64 KB covers hundreds of typed fields).\n const model = body?.model && typeof body.model === 'object' ? body.model : null\n if (model && JSON.stringify(model).length > 64 * 1024) {\n return Response.json({ error: 'Dataset model too large' }, { status: 413 })\n }\n /**\n * THE ENFORCEMENT POINT (AGL-2371, the AGL-2231 treatment): the count,\n * the decision and the create in ONE transaction.\n *\n * The check above counted, then the request went on awaiting, and only\n * then created — and every await is a yield, so N concurrent creates\n * each read the same pre-count, each found room, and each landed.\n * Nothing re-counts afterwards, so the extra datasets were permanent and\n * billed nothing. `tx.get` on the aggregate takes a pessimistic lock on\n * the documents the query matched, so the loser of a race retries,\n * re-reads the higher count and is refused.\n *\n * The ORG document is deliberately read outside: the plan and the\n * purchased add-ons are the other input to this decision, but neither is\n * client-writable, so locking the org doc on every dataset create would\n * buy contention rather than correctness. That is the AGL-2369 lesson\n * applied, not skipped — there the routing map *was* client-writable and\n * *did* change the count.\n *\n * The refusal comes back as data and is rendered outside: building a\n * response inside a body that can run several times reads as if the\n * transaction were a place effects happen.\n */\n const refusal = await firestore.runTransaction<string | null>(\n async (tx) => {\n const live = (await tx.get(datasetsRef.count())).data().count\n const authoritative = checkDatasetQuota(org, live)\n if (!authoritative.allowed) return overDatasetQuota(authoritative)\n tx.create(datasetsRef.doc(id), {\n displayName,\n fields,\n ...(model ? { model } : {}),\n // The scope decided above. Stamping SOMETHING is mandatory: an\n // `array-contains-any` matches nothing on a doc missing the field,\n // so an unstamped dataset renders on no site at all (AGL-1044).\n //\n // Through `newResourceScopeFields` since AGL-1484: AGL-1478 built\n // that helper as the type-level gate for exactly the collections\n // with no document constructor of their own — `datasets` named\n // first among them — and then only the marketplace fork used it,\n // while the two ordinary dataset creates went on writing the field\n // by hand. A required argument that three of four callers bypass\n // is a convention, not a guarantee.\n ...newResourceScopeFields(visibleTo),\n createdAt: Timestamp.now(),\n })\n return null\n },\n )\n if (refusal) return Response.json({ error: refusal }, { status: 403 })\n return Response.json({ ok: true, id }, { status: 200 })\n }\n\n if (\n action === 'create-record' ||\n action === 'import-records' ||\n action === 'announce-records' ||\n action === 'add-address-field'\n ) {\n const datasetId = String(body?.datasetId ?? '')\n if (!datasetId) {\n return Response.json({ error: 'Missing datasetId' }, { status: 400 })\n }\n const datasetRef = orgRef.collection('datasets').doc(datasetId)\n const datasetSnapshot = await datasetRef.get()\n if (!datasetSnapshot.exists) {\n return Response.json({ error: 'Unknown dataset' }, { status: 404 })\n }\n /**\n * The caller has to be able to SEE the dataset they add rows to.\n *\n * The role and `data.manage` gates above are about the org, and a member\n * scoped to one site passes both. The rules hold every record update and\n * delete to the parent dataset being visible (`parentDatasetVisible()`),\n * and refuse client record creates outright, so this route is the one\n * place a member's create can be held to the same condition. Without it,\n * a collaborator on one client's site could add rows, by id alone, to an\n * internal dataset shared only with other sites.\n *\n * The Admin SDK evaluates no rules, so `memberCanSee` IS the enforcement\n * here, as on the export route beside this one. Answered with the same\n * 404 as a dataset that does not exist: whether one outside the caller's\n * reach exists is not theirs to learn either.\n */\n if (\n decoded['staff'] !== true &&\n !memberCanSee(\n member,\n (datasetSnapshot.data() as { visibleTo?: string[] } | undefined)\n ?.visibleTo,\n )\n ) {\n return Response.json({ error: 'Unknown dataset' }, { status: 404 })\n }\n /**\n * A record the BROWSER wrote, announcing itself (AGL-3113).\n *\n * Record edits and deletes stay client-direct on purpose — they consume\n * no quota, so AGL-473 left them out of the server leg — which also left\n * them with nothing to announce from. They are the same staleness as a\n * create: a page listing the rows goes on showing the old one.\n *\n * A drop, and only a drop. It moves no pointer, writes nothing and\n * registers no route, so it grants nothing the rules withhold — and it\n * is gated by the same org membership and the same `memberCanSee` the\n * create is, which is what stops a collaborator on one site from\n * learning that another site's dataset exists.\n */\n if (action === 'announce-records') {\n const announced = await announceDatasetChange({\n firestore,\n orgId,\n datasetId,\n })\n return Response.json({ ok: true, announced }, { status: 200 })\n }\n const model = effectiveDatasetModel(datasetSnapshot.data() as any)\n /**\n * Gives the dataset a page address (AGL-3475): a \"Page address\" field\n * that fills in from `sourceField`, and an address on every record that\n * has none yet, unique within the dataset (`roofing`, `roofing-2`).\n *\n * Server-side because it writes every record, and each write has to\n * carry the filter fields the record template's lookup reads — the\n * address is found through `filterValues`, so a record written without\n * them would have a page nobody can reach. A dataset that already has\n * an address field keeps it: the call fills that field's empty\n * addresses instead of adding a second one.\n */\n if (action === 'add-address-field') {\n const sourceField = String(body?.sourceField ?? '').trim()\n const source = model.fields[sourceField]\n if (!source || source.type !== 'text' || isRecordAddressField(source)) {\n return Response.json(\n { error: 'Pick a text field to fill page addresses from' },\n { status: 400 },\n )\n }\n const held = recordAddressFieldIds(model)[0]\n const fieldId = held ?? defaultDatasetFieldId('slug', new Set(model.order))\n const nextModel = held\n ? model\n : {\n fields: {\n ...model.fields,\n [fieldId]: {\n name: 'Page address',\n type: 'text' as const,\n customType: RECORD_PAGE_ADDRESS_FIELD_TYPE,\n slugFrom: sourceField,\n },\n },\n order: [...model.order, fieldId],\n }\n if (!held) {\n await datasetRef.update({ model: nextModel, updatedAt: Timestamp.now() })\n }\n const snapshot = await datasetRef\n .collection('records')\n .orderBy(FieldPath.documentId())\n .limit(ADDRESS_FILL_MAX + 1)\n .get()\n const docs = snapshot.docs.slice(0, ADDRESS_FILL_MAX)\n const valuesOf = (doc: FirebaseFirestore.QueryDocumentSnapshot) =>\n (doc.get('values') ?? {}) as Record<string, unknown>\n const taken = docs\n .map((doc) => recordAddressOf(valuesOf(doc), fieldId))\n .filter(Boolean)\n const assigned = uniqueRecordAddresses(\n docs\n .filter((doc) => !recordAddressOf(valuesOf(doc), fieldId))\n .map((doc) => ({ id: doc.id, source: valuesOf(doc)[sourceField] })),\n taken,\n )\n const writes = docs.filter((doc) => assigned.has(doc.id))\n for (let start = 0; start < writes.length; start += IMPORT_CHUNK) {\n const batch = firestore.batch()\n for (const doc of writes.slice(start, start + IMPORT_CHUNK)) {\n const values = { ...valuesOf(doc), [fieldId]: assigned.get(doc.id) }\n batch.update(doc.ref, {\n values,\n ...datasetIntegrityUpdate(nextModel, values, FieldValue.delete()),\n updatedAt: Timestamp.now(),\n })\n }\n await batch.commit()\n }\n if (writes.length || !held) {\n await announceDatasetChange({ firestore, orgId, datasetId })\n }\n return Response.json(\n {\n ok: true,\n fieldId,\n filled: writes.length,\n truncated: snapshot.docs.length > ADDRESS_FILL_MAX,\n },\n { status: 200 },\n )\n }\n // Before either validation below: a plugin's field validator only runs\n // once its plugin's server entry has registered it.\n await ensureDeclaredCustomFieldTypes(model)\n const recordsRef = datasetRef.collection('records')\n const recordCount = (await recordsRef.count().get()).data().count\n const overRecordQuota = (limit: number) =>\n `Record limit reached (${limit}) — upgrade in Billing`\n\n if (action === 'create-record') {\n const coerced = fillRecordAddresses(\n model,\n coerceDocumentValues(model, body?.values ?? {}),\n )\n const errors = validateDocument(model, coerced)\n if (Object.keys(errors).length) {\n return Response.json({ error: 'Record failed validation', errors }, { status: 400 })\n }\n // The fast refusal. NOT the enforcement point — that is the\n // transaction below — but it keeps the refusal ORDER on this route\n // intact: a caller over BOTH the row cap and the byte band is told\n // about the rows, as they always were.\n const quota = checkQuota(org, 'recordsPerDataset', recordCount)\n if (!quota.allowed) {\n return Response.json({\n error: overRecordQuota(quota.limit),\n }, { status: 403 })\n }\n // Bytes, not rows (AGL-2163). Before the write, like every other\n // quota on this route.\n const storageRefusal = await refuseIfDataStorageBlocked(org, orgRef)\n if (storageRefusal) return storageRefusal\n const id = createResourceUid()\n /**\n * THE ENFORCEMENT POINT (AGL-2371, the AGL-2231 treatment): the count,\n * the decision and the create in ONE transaction.\n *\n * The check above counted, then `refuseIfDataStorageBlocked` awaited,\n * and only then did the row land. Every await is a yield, so N\n * concurrent creates each read the same pre-count, each found room,\n * and each landed — permanently, since nothing re-counts afterwards. A\n * free-with-rows-granted org's thousand rows became twenty thousand by\n * sending twenty requests at once.\n *\n * `tx.get` on the aggregate takes a pessimistic lock on the documents\n * the query matched, so the loser retries, re-reads the higher count\n * and is refused. `order` is taken from the transaction's own count\n * for the same reason: the pre-count is what made two concurrent rows\n * share an order.\n *\n * The BYTE gate stays outside. It is a different quota with its own\n * reads, it is not a function of the rows this transaction writes, and\n * re-running it on every retry attempt would put a multi-document read\n * inside a lock.\n */\n const refusal = await firestore.runTransaction<string | null>(\n async (tx) => {\n const live = (await tx.get(recordsRef.count())).data().count\n const authoritative = checkQuota(org, 'recordsPerDataset', live)\n if (!authoritative.allowed) return overRecordQuota(authoritative.limit)\n tx.create(recordsRef.doc(id), {\n values: coerced,\n // The integrity index the delete check queries. Written on the\n // same write as the values it describes — an index that lags\n // them reports a reference the record no longer holds, or\n // misses one it does.\n ...datasetIntegrityFields(model, coerced),\n order: live,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n })\n return null\n },\n )\n if (refusal) return Response.json({ error: refusal }, { status: 403 })\n // The record is stored; now the pages that repeat over it are told\n // (AGL-3113). Awaited so the site is already serving the new row by the\n // time the editor's save returns, and reported so a refused drop can\n // say the live pages are behind rather than leaving it to be noticed.\n const announced = await announceDatasetChange({\n firestore,\n orgId,\n datasetId,\n })\n return Response.json({ ok: true, id, announced }, { status: 200 })\n }\n\n // import-records: the console sends only the NEW rows (updates to\n // existing ids stay client-direct — they don't consume quota).\n const rows = Array.isArray(body?.records)\n ? (body.records as Array<{ values?: unknown }>)\n : []\n if (rows.length === 0) {\n return Response.json({ error: 'No records to import' }, { status: 400 })\n }\n const overImportQuota = (limit: number, needed: number) =>\n `This import needs ${needed} record slots — your plan ` +\n `allows ${limit} per dataset. See Billing to upgrade.`\n // The fast refusal, on the whole import. NOT the enforcement point —\n // that is the per-chunk transaction below — but it is what answers an\n // import that never fitted, before a single row is written.\n const quota = checkQuota(\n org,\n 'recordsPerDataset',\n recordCount + rows.length - 1,\n )\n if (!quota.allowed) {\n return Response.json({\n error: overImportQuota(quota.limit, rows.length),\n }, { status: 403 })\n }\n // Bytes, not rows (AGL-2163). An import is the path that moves the\n // most bytes in one request, so leaving it out would have left the\n // enforcement exactly where the customer does not meet it.\n const importStorageRefusal = await refuseIfDataStorageBlocked(org, orgRef)\n if (importStorageRefusal) return importStorageRefusal\n const prepared = rows.map((row, index) => {\n const coerced = fillRecordAddresses(\n model,\n coerceDocumentValues(\n model,\n (row?.values ?? {}) as Record<string, unknown>,\n ),\n )\n const errors = validateDocument(model, coerced)\n return { index, coerced, valid: Object.keys(errors).length === 0 }\n })\n const invalid = prepared.filter((row) => !row.valid)\n if (invalid.length) {\n return Response.json({\n error: `${invalid.length} rows failed validation`,\n rows: invalid.map((row) => row.index),\n }, { status: 400 })\n }\n /**\n * THE ENFORCEMENT POINT (AGL-2371): each chunk counts, decides and\n * writes inside ONE transaction.\n *\n * The import used to count once, decide once, and then commit a series\n * of `WriteBatch`es. A batch is atomic but NOT conditional on a read\n * taken before it, so two concurrent imports each priced themselves off\n * the same pre-count and both landed in full — the create leg's defect,\n * multiplied by the batch size.\n *\n * It cannot be ONE transaction for every import the way the single-row\n * create can: `recordsPerDataset` runs to a million, and a transaction\n * carries at most 500 writes. So the chunk is the unit, and each chunk's\n * count is re-read under the lock and re-checked against the plan. The\n * cap therefore holds ABSOLUTELY at every size — what a race can cost is\n * a partial import, not an overshoot, and a partial import was already\n * possible the moment a mid-import batch could fail.\n *\n * The ids are minted before the transaction so a retry re-creates the\n * SAME documents rather than a second copy of the chunk.\n *\n * This is not AGL-2370's problem in miniature. There the whole bundle\n * must land or none of it, so chunking cannot be the unit and a lease is\n * needed; here rows are independent and \"fewer rows than asked for\" is a\n * reportable outcome.\n */\n const ids: string[] = []\n let refusedAt: string | null = null\n for (\n let start = 0;\n start < prepared.length && refusedAt === null;\n start += IMPORT_CHUNK\n ) {\n const chunk = prepared.slice(start, start + IMPORT_CHUNK)\n const chunkIds = chunk.map(() => createResourceUid())\n refusedAt = await firestore.runTransaction<string | null>(async (tx) => {\n const live = (await tx.get(recordsRef.count())).data().count\n const authoritative = checkQuota(\n org,\n 'recordsPerDataset',\n live + chunk.length - 1,\n )\n if (!authoritative.allowed) {\n return overImportQuota(authoritative.limit, rows.length)\n }\n chunk.forEach((row, offset) => {\n tx.create(recordsRef.doc(chunkIds[offset]), {\n values: row.coerced,\n ...datasetIntegrityFields(model, row.coerced),\n order: live + offset,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n })\n })\n return null\n })\n if (refusedAt === null) ids.push(...chunkIds)\n }\n /**\n * ONE announce for the whole import, after the loop (AGL-3113).\n *\n * Not per chunk and certainly not per row: a thousand rows arrive as one\n * import and make the same pages stale once, so the pages are worked out\n * once and dropped once. The refusal path announces too — rows that\n * landed before the quota bit are live records, and leaving them out of\n * the refresh would make a partial import look like no import at all.\n */\n if (ids.length) {\n await announceDatasetChange({ firestore, orgId, datasetId })\n }\n if (refusedAt !== null) {\n // What landed is reported alongside the refusal. Rows are independent,\n // so silently discarding the created ids would tell the caller nothing\n // was written while the dataset says otherwise.\n return Response.json(\n { error: refusedAt, ids, created: ids.length },\n { status: 403 },\n )\n }\n return Response.json({ ok: true, ids, created: ids.length }, { status: 200 })\n }\n\n return Response.json({ error: 'Unknown action' }, { status: 400 })\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993); anything\n // else keeps the answer below.\n if (isRefusedIdToken(error)) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n console.error(error)\n return Response.json({ error: 'Dataset operation failed' }, { status: 500 })\n }\n}\n"],"names":["defaultScopeForNewResource","newResourceScopeFields","pluginRequestFromWeb","checkDatasetQuota","checkEntitlement","checkQuota","createResourceUid","ensureDeclaredCustomFieldTypes","memberCanSee","emailUnverifiedResponse","firebaseAdmin","dataStorageRefusal","isImpersonationSession","isServerReleaseFlagOnForOrg","lockdownRefusal","memberHasOrgPermission","resolveOrgIdForHost","resolveOrgMembership","isRefusedIdToken","FieldPath","FieldValue","Timestamp","announceDatasetRecords","announceDatasetChange","coerceDocumentValues","datasetIntegrityFields","datasetIntegrityUpdate","effectiveDatasetModel","validateDocument","defaultDatasetFieldId","RECORD_PAGE_ADDRESS_FIELD_TYPE","fillRecordAddresses","isRecordAddressField","recordAddressFieldIds","recordAddressOf","uniqueRecordAddresses","refuseIfDataStorageBlocked","org","orgRef","refusal","Response","json","error","basis","includedMb","status","WRITER_ROLES","Set","IMPORT_CHUNK","ADDRESS_FILL_MAX","datasetsHandler","request","headers","method","body","rawHeaders","authorization","idToken","startsWith","slice","length","undefined","orgId","String","action","decoded","app","auth","verifyIdToken","email_verified","membership","uid","member","has","role","firestore","collection","doc","orgSnapshot","get","exists","data","locked","staff","displayName","trim","fields","Array","isArray","map","field","hostId","includes","visibleTo","defaultResourceScope","datasetsRef","overDatasetQuota","quota","upgradeRequired","limit","addonPriceUsd","count","allowed","id","model","JSON","stringify","runTransaction","tx","live","authoritative","create","createdAt","now","ok","datasetSnapshot","datasetId","datasetRef","announced","sourceField","source","type","held","fieldId","order","nextModel","name","customType","slugFrom","update","updatedAt","snapshot","orderBy","documentId","docs","valuesOf","taken","filter","Boolean","assigned","writes","start","batch","values","ref","delete","commit","filled","truncated","recordsRef","recordCount","overRecordQuota","coerced","errors","Object","keys","storageRefusal","rows","records","overImportQuota","needed","importStorageRefusal","prepared","row","index","valid","invalid","ids","refusedAt","chunk","chunkIds","forEach","offset","push","created","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/datasets-route.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 defaultScopeForNewResource,\n newResourceScopeFields,\n pluginRequestFromWeb,\n type PluginWebApiHandler,\n} from '@aglyn/aglyn/server'\nimport {\n checkDatasetQuota,\n checkEntitlement,\n checkQuota,\n createResourceUid,\n ensureDeclaredCustomFieldTypes,\n memberCanSee,\n} from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n dataStorageRefusal,\n isImpersonationSession,\n isServerReleaseFlagOnForOrg,\n lockdownRefusal,\n memberHasOrgPermission,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { FieldPath, FieldValue, Timestamp } from 'firebase-admin/firestore'\nimport { announceDatasetRecords as announceDatasetChange } from './announce-dataset-records'\nimport { coerceDocumentValues, datasetIntegrityFields, datasetIntegrityUpdate, effectiveDatasetModel, validateDocument } from '../model/dataset-models'\nimport { defaultDatasetFieldId } from '../model/datasets'\nimport { planReferenceFixups, referencingFieldIds, restrictedDeleteMessage } from '../model/dataset-record-delete-plan'\nimport {\n RECORD_PAGE_ADDRESS_FIELD_TYPE,\n fillRecordAddresses,\n isRecordAddressField,\n recordAddressFieldIds,\n recordAddressOf,\n uniqueRecordAddresses,\n} from '../record-pages/record-pages'\n\n/**\n * `dataStorageMbPerOrg` for this route, rendered as the console's 403.\n *\n * The VERDICT moved to `utils/server/data-storage-gate` (AGL-2253) — it was\n * defined here, so `/v1` and the tenant form path, which write dataset bytes\n * through their own code, enforced nothing. Only the console wording is left\n * behind, because the other two callers answer different shapes.\n */\nasync function refuseIfDataStorageBlocked(\n org: unknown,\n orgRef: FirebaseFirestore.DocumentReference,\n): Promise<Response | null> {\n const refusal = await dataStorageRefusal(org, orgRef)\n if (!refusal) return null\n return Response.json(\n {\n error:\n refusal.basis === 'always'\n ? 'Dataset storage is not included on this plan — upgrade in Billing'\n : `Dataset storage limit reached (${refusal.includedMb} MB) — ` +\n 'upgrade in Billing',\n },\n { status: 403 },\n )\n}\n\n/** Roles allowed to create org data — mirrors rules' canWriteOrgData(). */\nconst WRITER_ROLES = new Set(['owner', 'admin', 'editor'])\n\n/**\n * Rows written per import transaction.\n *\n * Firestore allows at most 500 writes in one transaction, and this is the\n * chunk size the route already used for its batches, so nothing about the\n * shape of an import changed when the batches became transactions (AGL-2371).\n */\nconst IMPORT_CHUNK = 400\n\n/**\n * How many records one \"give it page addresses\" call fills (AGL-3475). Far\n * past a services or locations dataset; a larger one reports `truncated`, and\n * the next call fills the next records, since a filled record is skipped.\n */\nconst ADDRESS_FILL_MAX = 5000\n\n/**\n * Dataset/record creation API (AGL-473): creates moved out of the client\n * SDK so quotas and entitlements are enforced server-side — Firestore\n * rules deny client-side `create` on `orgs/{orgId}/datasets/**` (updates\n * and deletes stay client-direct, they don't consume quota). Actions:\n *\n * - `create-dataset`: `dataStore` entitlement + `checkDatasetQuota`\n * (addon-aware, org-scoped). An optional `hostId` names the site the\n * dataset is created from, which the org's Default sharing can narrow it\n * to.\n * - `create-record`: `recordsPerDataset` quota; values are re-coerced\n * and re-validated against the dataset's model server-side.\n * - `import-records`: batch create with the whole batch fitting the cap.\n * - `add-address-field`: a \"Page address\" field that fills in from a text\n * field, and a unique address on every record that has none (AGL-3475).\n * - `update-record` / `delete-record`: the Data card's record edit and\n * delete (with its reference integrity) for the native apps, which have no\n * browser to write from (AGL-3668).\n *\n * Since AGL-2163 both record actions ALSO enforce `checkDataStorageQuota`.\n * See {@link refuseIfDataStorageBlocked}: that check's `allowed` field had no\n * reader anywhere in the platform while its docblock said free \"hard-blocks\n * at the included size\", so the byte band was documentation. `recordsPerDataset`\n * counts ROWS and does not bound their size, so it was never the same limit.\n */\nexport const datasetsHandler: PluginWebApiHandler = async (request) => {\n const { method, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n\n const orgId = String(body?.orgId ?? '')\n const action = String(body?.action ?? '')\n if (!orgId) return Response.json({ error: 'Missing orgId' }, { status: 400 })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const member = membership?.member as any\n if (!member || !WRITER_ROLES.has(String(member.role))) {\n return Response.json({ error: 'Editing org data requires the editor role' }, { status: 403 })\n }\n /*==========================================\n * AND the granular permission (AGL-2444).\n *\n * `data.manage` had ZERO consumers: an owner could build a custom role\n * with \"Manage data\" unticked, assign it, and the member kept creating,\n * editing and deleting datasets — because this gate read the raw role,\n * exactly the defect AGL-2350 fixed one route over on `hosts.create`.\n *\n * Identical for the built-in roles — `data.manage` is on for owner,\n * admin and editor, which is `WRITER_ROLES` — so nobody's access moves.\n * What it adds is that a custom role or a per-member override is now\n * honoured, which is the narrowing the docs sell.\n *\n * The role check is kept ALONGSIDE it rather than replaced. It mirrors\n * the rules' `canWriteOrgData()`, and a route that disagreed with the\n * rules would let one door open while the other stayed shut.\n *=========================================*/\n if (\n decoded['staff'] !== true &&\n !(await memberHasOrgPermission(orgId, member, 'data.manage'))\n ) {\n return Response.json({\n error: 'Your organization role does not allow editing organization data',\n }, { status: 403 })\n }\n\n // Release gate (AGL-1653). `<FeatureGate flag=\"release_data_store\">` on\n // the org Data page was the only gate, so org-level dataset\n // administration kept working with the flag off — the milder sibling of\n // the add-on store leak above it in the same issue. Same staff bypass as\n // `FeatureGate`'s `visible`, same 404: released-off means absent.\n if (\n decoded['staff'] !== true &&\n !(await isServerReleaseFlagOnForOrg('release_data_store', orgId))\n ) {\n return Response.json({ error: 'Not available' }, { status: 404 })\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return Response.json({ error: 'Unknown organization' }, { status: 404 })\n }\n const org = orgSnapshot.data() as any\n\n // Lockdown verdict (AGL-1506): subsumes the bare `orgSuspended`\n // projection check this route used to make — the org doc is read just\n // above anyway, so the full verdict (platform/org/user scopes, staff\n // bypass, distinct 423 body) costs no extra org read here.\n const locked = await lockdownRefusal({\n request,\n staff: decoded['staff'] === true,\n uid: decoded.uid,\n org,\n })\n if (locked) return locked\n\n if (action === 'create-dataset') {\n if (!checkEntitlement(org, 'dataStore')) {\n return Response.json({\n error: 'Datasets require a Starter plan or higher',\n }, { status: 403 })\n }\n const displayName = String(body?.displayName ?? '').trim().slice(0, 120)\n const fields = Array.isArray(body?.fields)\n ? (body.fields as unknown[]).map((field) => String(field)).slice(0, 100)\n : []\n if (!displayName || fields.length === 0) {\n return Response.json({ error: 'Missing displayName or fields' }, { status: 400 })\n }\n /**\n * The site the dataset is created from — null on the organization Data\n * page, which has none.\n *\n * It reaches the document only through the org's Default sharing: with\n * `defaultResourceScope: 'host'` the new dataset is visible to this site\n * alone. That makes it a client claim deciding who can see the result,\n * so it is honored only when it holds on both counts:\n *\n * * The site is one of THIS org's. `hostIndex` is the mirror the Data\n * card resolved `orgId` from in the first place, so a create made\n * from a real site page cannot disagree with it. Another org's site\n * would stamp a token that no site and no scoped member of this org\n * holds: a dataset only the org's admins could ever find.\n * * The caller's own access reaches the scope it produces. A scoped\n * member naming a site outside their access would create a dataset\n * they cannot see afterwards, which is the create the rules'\n * `canCreateScoped()` refuses on the collections clients create.\n *\n * Both are refused rather than widened to All sites. Widening is the\n * direction that shows a dataset to sites nobody chose.\n */\n const hostId = String(body?.hostId ?? '').trim() || null\n if (\n hostId &&\n (hostId.includes('/') || (await resolveOrgIdForHost(hostId)) !== orgId)\n ) {\n return Response.json(\n { error: 'That site is not one of this organization’s' },\n { status: 400 },\n )\n }\n // Honors the org's `defaultResourceScope` (AGL-1048), falling back to\n // org-wide — which is also the only answer with no site in context.\n const visibleTo = defaultScopeForNewResource({\n defaultResourceScope: (org as {\n defaultResourceScope?: 'org' | 'host'\n })?.defaultResourceScope,\n hostId,\n })\n if (\n hostId &&\n decoded['staff'] !== true &&\n !memberCanSee(member, visibleTo)\n ) {\n return Response.json(\n { error: 'Your access to this organization does not reach that site' },\n { status: 403 },\n )\n }\n const datasetsRef = orgRef.collection('datasets')\n const overDatasetQuota = (quota: ReturnType<typeof checkDatasetQuota>) =>\n quota.upgradeRequired\n ? `Dataset limit reached (${quota.limit}) — upgrade in Billing`\n : `Dataset limit reached (${quota.limit}) — add extra datasets ` +\n `for $${quota.addonPriceUsd}/mo each or upgrade in Billing`\n // The fast refusal. NOT the enforcement point — that is the transaction\n // below — but it answers a caller who is simply over their limit before\n // the request builds anything.\n const quota = checkDatasetQuota(\n org,\n (await datasetsRef.count().get()).data().count,\n )\n if (!quota.allowed) {\n return Response.json({ error: overDatasetQuota(quota) }, { status: 403 })\n }\n const id = createResourceUid()\n // The model rides from the console (deriveModelFromFields output or\n // the join-collection template). `model` is independent of `fields`,\n // so cap its serialized size explicitly (a legit model is well under\n // this — 64 KB covers hundreds of typed fields).\n const model = body?.model && typeof body.model === 'object' ? body.model : null\n if (model && JSON.stringify(model).length > 64 * 1024) {\n return Response.json({ error: 'Dataset model too large' }, { status: 413 })\n }\n /**\n * THE ENFORCEMENT POINT (AGL-2371, the AGL-2231 treatment): the count,\n * the decision and the create in ONE transaction.\n *\n * The check above counted, then the request went on awaiting, and only\n * then created — and every await is a yield, so N concurrent creates\n * each read the same pre-count, each found room, and each landed.\n * Nothing re-counts afterwards, so the extra datasets were permanent and\n * billed nothing. `tx.get` on the aggregate takes a pessimistic lock on\n * the documents the query matched, so the loser of a race retries,\n * re-reads the higher count and is refused.\n *\n * The ORG document is deliberately read outside: the plan and the\n * purchased add-ons are the other input to this decision, but neither is\n * client-writable, so locking the org doc on every dataset create would\n * buy contention rather than correctness. That is the AGL-2369 lesson\n * applied, not skipped — there the routing map *was* client-writable and\n * *did* change the count.\n *\n * The refusal comes back as data and is rendered outside: building a\n * response inside a body that can run several times reads as if the\n * transaction were a place effects happen.\n */\n const refusal = await firestore.runTransaction<string | null>(\n async (tx) => {\n const live = (await tx.get(datasetsRef.count())).data().count\n const authoritative = checkDatasetQuota(org, live)\n if (!authoritative.allowed) return overDatasetQuota(authoritative)\n tx.create(datasetsRef.doc(id), {\n displayName,\n fields,\n ...(model ? { model } : {}),\n // The scope decided above. Stamping SOMETHING is mandatory: an\n // `array-contains-any` matches nothing on a doc missing the field,\n // so an unstamped dataset renders on no site at all (AGL-1044).\n //\n // Through `newResourceScopeFields` since AGL-1484: AGL-1478 built\n // that helper as the type-level gate for exactly the collections\n // with no document constructor of their own — `datasets` named\n // first among them — and then only the marketplace fork used it,\n // while the two ordinary dataset creates went on writing the field\n // by hand. A required argument that three of four callers bypass\n // is a convention, not a guarantee.\n ...newResourceScopeFields(visibleTo),\n createdAt: Timestamp.now(),\n })\n return null\n },\n )\n if (refusal) return Response.json({ error: refusal }, { status: 403 })\n return Response.json({ ok: true, id }, { status: 200 })\n }\n\n if (\n action === 'create-record' ||\n action === 'import-records' ||\n action === 'announce-records' ||\n action === 'add-address-field' ||\n action === 'update-record' ||\n action === 'delete-record'\n ) {\n const datasetId = String(body?.datasetId ?? '')\n if (!datasetId) {\n return Response.json({ error: 'Missing datasetId' }, { status: 400 })\n }\n const datasetRef = orgRef.collection('datasets').doc(datasetId)\n const datasetSnapshot = await datasetRef.get()\n if (!datasetSnapshot.exists) {\n return Response.json({ error: 'Unknown dataset' }, { status: 404 })\n }\n /**\n * The caller has to be able to SEE the dataset they add rows to.\n *\n * The role and `data.manage` gates above are about the org, and a member\n * scoped to one site passes both. The rules hold every record update and\n * delete to the parent dataset being visible (`parentDatasetVisible()`),\n * and refuse client record creates outright, so this route is the one\n * place a member's create can be held to the same condition. Without it,\n * a collaborator on one client's site could add rows, by id alone, to an\n * internal dataset shared only with other sites.\n *\n * The Admin SDK evaluates no rules, so `memberCanSee` IS the enforcement\n * here, as on the export route beside this one. Answered with the same\n * 404 as a dataset that does not exist: whether one outside the caller's\n * reach exists is not theirs to learn either.\n */\n if (\n decoded['staff'] !== true &&\n !memberCanSee(\n member,\n (datasetSnapshot.data() as { visibleTo?: string[] } | undefined)\n ?.visibleTo,\n )\n ) {\n return Response.json({ error: 'Unknown dataset' }, { status: 404 })\n }\n /**\n * A record the BROWSER wrote, announcing itself (AGL-3113).\n *\n * Record edits and deletes stay client-direct on purpose — they consume\n * no quota, so AGL-473 left them out of the server leg — which also left\n * them with nothing to announce from. They are the same staleness as a\n * create: a page listing the rows goes on showing the old one.\n *\n * A drop, and only a drop. It moves no pointer, writes nothing and\n * registers no route, so it grants nothing the rules withhold — and it\n * is gated by the same org membership and the same `memberCanSee` the\n * create is, which is what stops a collaborator on one site from\n * learning that another site's dataset exists.\n */\n if (action === 'announce-records') {\n const announced = await announceDatasetChange({\n firestore,\n orgId,\n datasetId,\n })\n return Response.json({ ok: true, announced }, { status: 200 })\n }\n const model = effectiveDatasetModel(datasetSnapshot.data() as any)\n /**\n * Gives the dataset a page address (AGL-3475): a \"Page address\" field\n * that fills in from `sourceField`, and an address on every record that\n * has none yet, unique within the dataset (`roofing`, `roofing-2`).\n *\n * Server-side because it writes every record, and each write has to\n * carry the filter fields the record template's lookup reads — the\n * address is found through `filterValues`, so a record written without\n * them would have a page nobody can reach. A dataset that already has\n * an address field keeps it: the call fills that field's empty\n * addresses instead of adding a second one.\n */\n if (action === 'add-address-field') {\n const sourceField = String(body?.sourceField ?? '').trim()\n const source = model.fields[sourceField]\n if (!source || source.type !== 'text' || isRecordAddressField(source)) {\n return Response.json(\n { error: 'Pick a text field to fill page addresses from' },\n { status: 400 },\n )\n }\n const held = recordAddressFieldIds(model)[0]\n const fieldId = held ?? defaultDatasetFieldId('slug', new Set(model.order))\n const nextModel = held\n ? model\n : {\n fields: {\n ...model.fields,\n [fieldId]: {\n name: 'Page address',\n type: 'text' as const,\n customType: RECORD_PAGE_ADDRESS_FIELD_TYPE,\n slugFrom: sourceField,\n },\n },\n order: [...model.order, fieldId],\n }\n if (!held) {\n await datasetRef.update({ model: nextModel, updatedAt: Timestamp.now() })\n }\n const snapshot = await datasetRef\n .collection('records')\n .orderBy(FieldPath.documentId())\n .limit(ADDRESS_FILL_MAX + 1)\n .get()\n const docs = snapshot.docs.slice(0, ADDRESS_FILL_MAX)\n const valuesOf = (doc: FirebaseFirestore.QueryDocumentSnapshot) =>\n (doc.get('values') ?? {}) as Record<string, unknown>\n const taken = docs\n .map((doc) => recordAddressOf(valuesOf(doc), fieldId))\n .filter(Boolean)\n const assigned = uniqueRecordAddresses(\n docs\n .filter((doc) => !recordAddressOf(valuesOf(doc), fieldId))\n .map((doc) => ({ id: doc.id, source: valuesOf(doc)[sourceField] })),\n taken,\n )\n const writes = docs.filter((doc) => assigned.has(doc.id))\n for (let start = 0; start < writes.length; start += IMPORT_CHUNK) {\n const batch = firestore.batch()\n for (const doc of writes.slice(start, start + IMPORT_CHUNK)) {\n const values = { ...valuesOf(doc), [fieldId]: assigned.get(doc.id) }\n batch.update(doc.ref, {\n values,\n ...datasetIntegrityUpdate(nextModel, values, FieldValue.delete()),\n updatedAt: Timestamp.now(),\n })\n }\n await batch.commit()\n }\n if (writes.length || !held) {\n await announceDatasetChange({ firestore, orgId, datasetId })\n }\n return Response.json(\n {\n ok: true,\n fieldId,\n filled: writes.length,\n truncated: snapshot.docs.length > ADDRESS_FILL_MAX,\n },\n { status: 200 },\n )\n }\n // Before either validation below: a plugin's field validator only runs\n // once its plugin's server entry has registered it.\n await ensureDeclaredCustomFieldTypes(model)\n const recordsRef = datasetRef.collection('records')\n\n /**\n * A record edit (AGL-3668): the Data card's Save on an existing record,\n * for a caller that is not a browser. The card writes it straight to\n * Firestore — it consumes no quota — with the values coerced, filled\n * and validated against the model and the integrity and filter index\n * moved with them; the native apps reach the same write here, under\n * the same role, `data.manage` and visibility gates as a create.\n * `values` is replaced whole, so a removed field's value strips here,\n * as it does on the card (the AGL-178 policy).\n */\n if (action === 'update-record') {\n const recordId = String(body?.recordId ?? '')\n if (!recordId) {\n return Response.json({ error: 'Missing recordId' }, { status: 400 })\n }\n const recordRef = recordsRef.doc(recordId)\n if (!(await recordRef.get()).exists) {\n return Response.json({ error: 'Unknown record' }, { status: 404 })\n }\n const coerced = fillRecordAddresses(\n model,\n coerceDocumentValues(model, body?.values ?? {}),\n )\n const errors = validateDocument(model, coerced)\n if (Object.keys(errors).length) {\n return Response.json({ error: 'Record failed validation', errors }, { status: 400 })\n }\n await recordRef.update({\n values: coerced,\n ...datasetIntegrityUpdate(model, coerced, FieldValue.delete()),\n updatedAt: Timestamp.now(),\n })\n const announced = await announceDatasetChange({ firestore, orgId, datasetId })\n return Response.json({ ok: true, id: recordId, announced }, { status: 200 })\n }\n\n /**\n * A record delete (AGL-3668), with the card's delete integrity\n * (AGL-180): every dataset the caller can see whose model references\n * this one is asked, by its `referencedIds` index, whether it still\n * points at the record. `restrict` refuses the delete and writes\n * nothing; `setNull` strips the reference from each holder first. The\n * verdict is `planReferenceFixups`, the one the card applies. The\n * datasets consulted are the ones the caller can see, exactly the set\n * the card reads, so the app is refused exactly when the console is.\n */\n if (action === 'delete-record') {\n const recordId = String(body?.recordId ?? '')\n if (!recordId) {\n return Response.json({ error: 'Missing recordId' }, { status: 400 })\n }\n const recordRef = recordsRef.doc(recordId)\n if (!(await recordRef.get()).exists) {\n return Response.json({ error: 'Unknown record' }, { status: 404 })\n }\n const others = (await orgRef.collection('datasets').get()).docs.filter(\n (other) =>\n decoded['staff'] === true ||\n memberCanSee(member, (other.data() as { visibleTo?: string[] }).visibleTo),\n )\n const fixups: Array<{ datasetId: string; model: ReturnType<typeof effectiveDatasetModel>; updates: Array<{ id: string; values: Record<string, unknown> }> }> = []\n for (const other of others) {\n const otherModel = effectiveDatasetModel(other.data() as any)\n if (!referencingFieldIds(otherModel, datasetId).length) continue\n const holders = await other.ref\n .collection('records')\n .where('referencedIds', 'array-contains', recordId)\n .get()\n const plan = planReferenceFixups(\n otherModel,\n datasetId,\n recordId,\n holders.docs.map((holder) => ({ id: holder.id, values: holder.get('values') })),\n )\n if (plan.kind === 'restricted') {\n const name = String((other.data() as any)?.displayName ?? (other.data() as any)?.name ?? '').trim()\n return Response.json(\n { error: restrictedDeleteMessage(plan.holders, name) },\n { status: 409 },\n )\n }\n if (plan.kind === 'strip') fixups.push({ datasetId: other.id, model: otherModel, updates: plan.updates })\n }\n for (const fixup of fixups) {\n const holdersRef = orgRef.collection('datasets').doc(fixup.datasetId).collection('records')\n for (let start = 0; start < fixup.updates.length; start += IMPORT_CHUNK) {\n const batch = firestore.batch()\n for (const update of fixup.updates.slice(start, start + IMPORT_CHUNK)) {\n batch.update(holdersRef.doc(update.id), {\n values: update.values,\n ...datasetIntegrityUpdate(fixup.model, update.values, FieldValue.delete()),\n })\n }\n await batch.commit()\n }\n }\n await recordRef.delete()\n for (const changed of [datasetId, ...fixups.map((fixup) => fixup.datasetId)]) {\n await announceDatasetChange({ firestore, orgId, datasetId: changed })\n }\n return Response.json(\n { ok: true, id: recordId, fixed: fixups.reduce((total, fixup) => total + fixup.updates.length, 0) },\n { status: 200 },\n )\n }\n\n const recordCount = (await recordsRef.count().get()).data().count\n const overRecordQuota = (limit: number) =>\n `Record limit reached (${limit}) — upgrade in Billing`\n\n if (action === 'create-record') {\n const coerced = fillRecordAddresses(\n model,\n coerceDocumentValues(model, body?.values ?? {}),\n )\n const errors = validateDocument(model, coerced)\n if (Object.keys(errors).length) {\n return Response.json({ error: 'Record failed validation', errors }, { status: 400 })\n }\n // The fast refusal. NOT the enforcement point — that is the\n // transaction below — but it keeps the refusal ORDER on this route\n // intact: a caller over BOTH the row cap and the byte band is told\n // about the rows, as they always were.\n const quota = checkQuota(org, 'recordsPerDataset', recordCount)\n if (!quota.allowed) {\n return Response.json({\n error: overRecordQuota(quota.limit),\n }, { status: 403 })\n }\n // Bytes, not rows (AGL-2163). Before the write, like every other\n // quota on this route.\n const storageRefusal = await refuseIfDataStorageBlocked(org, orgRef)\n if (storageRefusal) return storageRefusal\n const id = createResourceUid()\n /**\n * THE ENFORCEMENT POINT (AGL-2371, the AGL-2231 treatment): the count,\n * the decision and the create in ONE transaction.\n *\n * The check above counted, then `refuseIfDataStorageBlocked` awaited,\n * and only then did the row land. Every await is a yield, so N\n * concurrent creates each read the same pre-count, each found room,\n * and each landed — permanently, since nothing re-counts afterwards. A\n * free-with-rows-granted org's thousand rows became twenty thousand by\n * sending twenty requests at once.\n *\n * `tx.get` on the aggregate takes a pessimistic lock on the documents\n * the query matched, so the loser retries, re-reads the higher count\n * and is refused. `order` is taken from the transaction's own count\n * for the same reason: the pre-count is what made two concurrent rows\n * share an order.\n *\n * The BYTE gate stays outside. It is a different quota with its own\n * reads, it is not a function of the rows this transaction writes, and\n * re-running it on every retry attempt would put a multi-document read\n * inside a lock.\n */\n const refusal = await firestore.runTransaction<string | null>(\n async (tx) => {\n const live = (await tx.get(recordsRef.count())).data().count\n const authoritative = checkQuota(org, 'recordsPerDataset', live)\n if (!authoritative.allowed) return overRecordQuota(authoritative.limit)\n tx.create(recordsRef.doc(id), {\n values: coerced,\n // The integrity index the delete check queries. Written on the\n // same write as the values it describes — an index that lags\n // them reports a reference the record no longer holds, or\n // misses one it does.\n ...datasetIntegrityFields(model, coerced),\n order: live,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n })\n return null\n },\n )\n if (refusal) return Response.json({ error: refusal }, { status: 403 })\n // The record is stored; now the pages that repeat over it are told\n // (AGL-3113). Awaited so the site is already serving the new row by the\n // time the editor's save returns, and reported so a refused drop can\n // say the live pages are behind rather than leaving it to be noticed.\n const announced = await announceDatasetChange({\n firestore,\n orgId,\n datasetId,\n })\n return Response.json({ ok: true, id, announced }, { status: 200 })\n }\n\n // import-records: the console sends only the NEW rows (updates to\n // existing ids stay client-direct — they don't consume quota).\n const rows = Array.isArray(body?.records)\n ? (body.records as Array<{ values?: unknown }>)\n : []\n if (rows.length === 0) {\n return Response.json({ error: 'No records to import' }, { status: 400 })\n }\n const overImportQuota = (limit: number, needed: number) =>\n `This import needs ${needed} record slots — your plan ` +\n `allows ${limit} per dataset. See Billing to upgrade.`\n // The fast refusal, on the whole import. NOT the enforcement point —\n // that is the per-chunk transaction below — but it is what answers an\n // import that never fitted, before a single row is written.\n const quota = checkQuota(\n org,\n 'recordsPerDataset',\n recordCount + rows.length - 1,\n )\n if (!quota.allowed) {\n return Response.json({\n error: overImportQuota(quota.limit, rows.length),\n }, { status: 403 })\n }\n // Bytes, not rows (AGL-2163). An import is the path that moves the\n // most bytes in one request, so leaving it out would have left the\n // enforcement exactly where the customer does not meet it.\n const importStorageRefusal = await refuseIfDataStorageBlocked(org, orgRef)\n if (importStorageRefusal) return importStorageRefusal\n const prepared = rows.map((row, index) => {\n const coerced = fillRecordAddresses(\n model,\n coerceDocumentValues(\n model,\n (row?.values ?? {}) as Record<string, unknown>,\n ),\n )\n const errors = validateDocument(model, coerced)\n return { index, coerced, valid: Object.keys(errors).length === 0 }\n })\n const invalid = prepared.filter((row) => !row.valid)\n if (invalid.length) {\n return Response.json({\n error: `${invalid.length} rows failed validation`,\n rows: invalid.map((row) => row.index),\n }, { status: 400 })\n }\n /**\n * THE ENFORCEMENT POINT (AGL-2371): each chunk counts, decides and\n * writes inside ONE transaction.\n *\n * The import used to count once, decide once, and then commit a series\n * of `WriteBatch`es. A batch is atomic but NOT conditional on a read\n * taken before it, so two concurrent imports each priced themselves off\n * the same pre-count and both landed in full — the create leg's defect,\n * multiplied by the batch size.\n *\n * It cannot be ONE transaction for every import the way the single-row\n * create can: `recordsPerDataset` runs to a million, and a transaction\n * carries at most 500 writes. So the chunk is the unit, and each chunk's\n * count is re-read under the lock and re-checked against the plan. The\n * cap therefore holds ABSOLUTELY at every size — what a race can cost is\n * a partial import, not an overshoot, and a partial import was already\n * possible the moment a mid-import batch could fail.\n *\n * The ids are minted before the transaction so a retry re-creates the\n * SAME documents rather than a second copy of the chunk.\n *\n * This is not AGL-2370's problem in miniature. There the whole bundle\n * must land or none of it, so chunking cannot be the unit and a lease is\n * needed; here rows are independent and \"fewer rows than asked for\" is a\n * reportable outcome.\n */\n const ids: string[] = []\n let refusedAt: string | null = null\n for (\n let start = 0;\n start < prepared.length && refusedAt === null;\n start += IMPORT_CHUNK\n ) {\n const chunk = prepared.slice(start, start + IMPORT_CHUNK)\n const chunkIds = chunk.map(() => createResourceUid())\n refusedAt = await firestore.runTransaction<string | null>(async (tx) => {\n const live = (await tx.get(recordsRef.count())).data().count\n const authoritative = checkQuota(\n org,\n 'recordsPerDataset',\n live + chunk.length - 1,\n )\n if (!authoritative.allowed) {\n return overImportQuota(authoritative.limit, rows.length)\n }\n chunk.forEach((row, offset) => {\n tx.create(recordsRef.doc(chunkIds[offset]), {\n values: row.coerced,\n ...datasetIntegrityFields(model, row.coerced),\n order: live + offset,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n })\n })\n return null\n })\n if (refusedAt === null) ids.push(...chunkIds)\n }\n /**\n * ONE announce for the whole import, after the loop (AGL-3113).\n *\n * Not per chunk and certainly not per row: a thousand rows arrive as one\n * import and make the same pages stale once, so the pages are worked out\n * once and dropped once. The refusal path announces too — rows that\n * landed before the quota bit are live records, and leaving them out of\n * the refresh would make a partial import look like no import at all.\n */\n if (ids.length) {\n await announceDatasetChange({ firestore, orgId, datasetId })\n }\n if (refusedAt !== null) {\n // What landed is reported alongside the refusal. Rows are independent,\n // so silently discarding the created ids would tell the caller nothing\n // was written while the dataset says otherwise.\n return Response.json(\n { error: refusedAt, ids, created: ids.length },\n { status: 403 },\n )\n }\n return Response.json({ ok: true, ids, created: ids.length }, { status: 200 })\n }\n\n return Response.json({ error: 'Unknown action' }, { status: 400 })\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993); anything\n // else keeps the answer below.\n if (isRefusedIdToken(error)) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n console.error(error)\n return Response.json({ error: 'Dataset operation failed' }, { status: 500 })\n }\n}\n"],"names":["defaultScopeForNewResource","newResourceScopeFields","pluginRequestFromWeb","checkDatasetQuota","checkEntitlement","checkQuota","createResourceUid","ensureDeclaredCustomFieldTypes","memberCanSee","emailUnverifiedResponse","firebaseAdmin","dataStorageRefusal","isImpersonationSession","isServerReleaseFlagOnForOrg","lockdownRefusal","memberHasOrgPermission","resolveOrgIdForHost","resolveOrgMembership","isRefusedIdToken","FieldPath","FieldValue","Timestamp","announceDatasetRecords","announceDatasetChange","coerceDocumentValues","datasetIntegrityFields","datasetIntegrityUpdate","effectiveDatasetModel","validateDocument","defaultDatasetFieldId","planReferenceFixups","referencingFieldIds","restrictedDeleteMessage","RECORD_PAGE_ADDRESS_FIELD_TYPE","fillRecordAddresses","isRecordAddressField","recordAddressFieldIds","recordAddressOf","uniqueRecordAddresses","refuseIfDataStorageBlocked","org","orgRef","refusal","Response","json","error","basis","includedMb","status","WRITER_ROLES","Set","IMPORT_CHUNK","ADDRESS_FILL_MAX","datasetsHandler","request","headers","method","body","rawHeaders","authorization","idToken","startsWith","slice","length","undefined","orgId","String","action","decoded","app","auth","verifyIdToken","email_verified","membership","uid","member","has","role","firestore","collection","doc","orgSnapshot","get","exists","data","locked","staff","displayName","trim","fields","Array","isArray","map","field","hostId","includes","visibleTo","defaultResourceScope","datasetsRef","overDatasetQuota","quota","upgradeRequired","limit","addonPriceUsd","count","allowed","id","model","JSON","stringify","runTransaction","tx","live","authoritative","create","createdAt","now","ok","datasetSnapshot","datasetId","datasetRef","announced","sourceField","source","type","held","fieldId","order","nextModel","name","customType","slugFrom","update","updatedAt","snapshot","orderBy","documentId","docs","valuesOf","taken","filter","Boolean","assigned","writes","start","batch","values","ref","delete","commit","filled","truncated","recordsRef","recordId","recordRef","coerced","errors","Object","keys","others","other","fixups","otherModel","holders","where","plan","holder","kind","push","updates","fixup","holdersRef","changed","fixed","reduce","total","recordCount","overRecordQuota","storageRefusal","rows","records","overImportQuota","needed","importStorageRefusal","prepared","row","index","valid","invalid","ids","refusedAt","chunk","chunkIds","forEach","offset","created","console"],"mappings":"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"}
|