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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +9 -6
  2. package/package.json +10 -10
  3. package/src/index.d.ts +1 -1
  4. package/src/index.js +1 -1
  5. package/src/index.js.map +1 -1
  6. package/src/lib/artifact/dataset-schema-artifact.server.d.ts +57 -0
  7. package/src/lib/artifact/dataset-schema-artifact.server.js +308 -0
  8. package/src/lib/artifact/dataset-schema-artifact.server.js.map +1 -0
  9. package/src/lib/artifact/dataset-schema.d.ts +136 -0
  10. package/src/lib/artifact/dataset-schema.js +232 -0
  11. package/src/lib/artifact/dataset-schema.js.map +1 -0
  12. package/src/lib/components/dataset-record-dialog.component.d.ts +1 -1
  13. package/src/lib/components/dataset-record-dialog.component.js +1 -1
  14. package/src/lib/components/dataset-record-dialog.component.js.map +1 -1
  15. package/src/lib/components/dataset-record-filter.d.ts +1 -1
  16. package/src/lib/components/dataset-record-filter.js +2 -2
  17. package/src/lib/components/dataset-record-filter.js.map +1 -1
  18. package/src/lib/components/dataset-schema-dialog.component.d.ts +1 -1
  19. package/src/lib/components/dataset-schema-dialog.component.js +55 -23
  20. package/src/lib/components/dataset-schema-dialog.component.js.map +1 -1
  21. package/src/lib/components/host-datasets-card.component.js +91 -403
  22. package/src/lib/components/host-datasets-card.component.js.map +1 -1
  23. package/src/lib/constants/bundle-common.d.ts +12 -0
  24. package/src/lib/constants/bundle-common.js +13 -0
  25. package/src/lib/constants/bundle-common.js.map +1 -1
  26. package/src/lib/declarations.console-server.d.ts +16 -1
  27. package/src/lib/declarations.console-server.js +34 -2
  28. package/src/lib/declarations.console-server.js.map +1 -1
  29. package/src/lib/declarations.server.d.ts +13 -0
  30. package/src/lib/declarations.server.js +48 -2
  31. package/src/lib/declarations.server.js.map +1 -1
  32. package/src/lib/form-target/dataset-form-record-target.server.js +5 -3
  33. package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -1
  34. package/src/lib/model/dataset-models.d.ts +392 -0
  35. package/src/lib/model/dataset-models.js +806 -0
  36. package/src/lib/model/dataset-models.js.map +1 -0
  37. package/src/lib/model/dataset-record-view.d.ts +1 -1
  38. package/src/lib/model/dataset-record-view.js +1 -2
  39. package/src/lib/model/dataset-record-view.js.map +1 -1
  40. package/src/lib/model/dataset-record.js +2 -2
  41. package/src/lib/model/dataset-record.js.map +1 -1
  42. package/src/lib/model/datasets.d.ts +162 -0
  43. package/src/lib/model/datasets.js +227 -0
  44. package/src/lib/model/datasets.js.map +1 -0
  45. package/src/lib/plugin.d.ts +4 -0
  46. package/src/lib/plugin.js +40 -0
  47. package/src/lib/plugin.js.map +1 -1
  48. package/src/lib/record-pages/record-page-live-paths.server.d.ts +55 -0
  49. package/src/lib/record-pages/record-page-live-paths.server.js +83 -0
  50. package/src/lib/record-pages/record-page-live-paths.server.js.map +1 -0
  51. package/src/lib/record-pages/record-page-read.server.d.ts +70 -0
  52. package/src/lib/record-pages/record-page-read.server.js +175 -0
  53. package/src/lib/record-pages/record-page-read.server.js.map +1 -0
  54. package/src/lib/record-pages/record-page-resolver.server.d.ts +37 -0
  55. package/src/lib/record-pages/record-page-resolver.server.js +131 -0
  56. package/src/lib/record-pages/record-page-resolver.server.js.map +1 -0
  57. package/src/lib/record-pages/record-page-sitemap.server.d.ts +32 -0
  58. package/src/lib/record-pages/record-page-sitemap.server.js +102 -0
  59. package/src/lib/record-pages/record-page-sitemap.server.js.map +1 -0
  60. package/src/lib/record-pages/record-page-source.d.ts +42 -0
  61. package/src/lib/record-pages/record-page-source.js +176 -0
  62. package/src/lib/record-pages/record-page-source.js.map +1 -0
  63. package/src/lib/record-pages/record-pages-route.d.ts +42 -0
  64. package/src/lib/record-pages/record-pages-route.js +348 -0
  65. package/src/lib/record-pages/record-pages-route.js.map +1 -0
  66. package/src/lib/record-pages/record-pages.d.ts +218 -0
  67. package/src/lib/record-pages/record-pages.js +351 -0
  68. package/src/lib/record-pages/record-pages.js.map +1 -0
  69. package/src/lib/record-pages/record-template-row-chip.component.d.ts +28 -0
  70. package/src/lib/record-pages/record-template-row-chip.component.js +87 -0
  71. package/src/lib/record-pages/record-template-row-chip.component.js.map +1 -0
  72. package/src/lib/record-pages/record-template-section.component.d.ts +30 -0
  73. package/src/lib/record-pages/record-template-section.component.js +491 -0
  74. package/src/lib/record-pages/record-template-section.component.js.map +1 -0
  75. package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -1
  76. package/src/lib/repeat/dataset-repeat-rows.js +30 -12
  77. package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
  78. package/src/lib/repeat/dataset-repeat-rows.server.d.ts +1 -1
  79. package/src/lib/repeat/dataset-repeat-rows.server.js +23 -12
  80. package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -1
  81. package/src/lib/repeat/repeat-record-pages.d.ts +48 -0
  82. package/src/lib/repeat/repeat-record-pages.js +64 -0
  83. package/src/lib/repeat/repeat-record-pages.js.map +1 -0
  84. package/src/lib/server/announce-dataset-records.d.ts +1 -1
  85. package/src/lib/server/announce-dataset-records.js +1 -1
  86. package/src/lib/server/announce-dataset-records.js.map +1 -1
  87. package/src/lib/server/api-v1/datasets.js +5 -3
  88. package/src/lib/server/api-v1/datasets.js.map +1 -1
  89. package/src/lib/server/dataset-figures.js +2 -2
  90. package/src/lib/server/dataset-figures.js.map +1 -1
  91. package/src/lib/server/dataset-live-pages.d.ts +147 -0
  92. package/src/lib/server/dataset-live-pages.js +436 -0
  93. package/src/lib/server/dataset-live-pages.js.map +1 -0
  94. package/src/lib/server/dataset-steps.server.d.ts +45 -0
  95. package/src/lib/server/dataset-steps.server.js +268 -0
  96. package/src/lib/server/dataset-steps.server.js.map +1 -0
  97. package/src/lib/server/datasets-route.d.ts +2 -0
  98. package/src/lib/server/datasets-route.js +102 -6
  99. package/src/lib/server/datasets-route.js.map +1 -1
  100. package/src/lib/server/resolve-dataset.d.ts +40 -0
  101. package/src/lib/server/resolve-dataset.js +64 -0
  102. package/src/lib/server/resolve-dataset.js.map +1 -0
  103. package/src/lib/server.d.ts +3 -2
  104. package/src/lib/server.js +8 -9
  105. package/src/lib/server.js.map +1 -1
  106. package/src/lib/site-bundle/datasets-package.d.ts +42 -0
  107. package/src/lib/site-bundle/datasets-package.js +95 -0
  108. package/src/lib/site-bundle/datasets-package.js.map +1 -0
  109. package/src/lib/site-bundle/datasets-site-bundle.server.js +2 -2
  110. package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -1
  111. package/src/lib/{model/index.js → tenant-api.d.ts} +12 -7
  112. package/src/lib/tenant-api.js +30 -0
  113. package/src/lib/tenant-api.js.map +1 -0
  114. package/src/lib/transfer/dataset-transfer-key.d.ts +30 -0
  115. package/src/lib/transfer/dataset-transfer-key.js +31 -0
  116. package/src/lib/transfer/dataset-transfer-key.js.map +1 -0
  117. package/src/lib/transfer/dataset-transfer-model.d.ts +146 -0
  118. package/src/lib/transfer/dataset-transfer-model.js +536 -0
  119. package/src/lib/transfer/dataset-transfer-model.js.map +1 -0
  120. package/src/lib/{model/index.d.ts → transfer/dataset-transfer-resource.d.ts} +2 -6
  121. package/src/lib/transfer/dataset-transfer-resource.js +41 -0
  122. package/src/lib/transfer/dataset-transfer-resource.js.map +1 -0
  123. package/src/lib/transfer/dataset-transfer.server.d.ts +69 -0
  124. package/src/lib/transfer/dataset-transfer.server.js +930 -0
  125. package/src/lib/transfer/dataset-transfer.server.js.map +1 -0
  126. package/src/lib/model/dataset-csv.d.ts +0 -36
  127. package/src/lib/model/dataset-csv.js +0 -85
  128. package/src/lib/model/dataset-csv.js.map +0 -1
  129. package/src/lib/model/dataset-io.d.ts +0 -44
  130. package/src/lib/model/dataset-io.js +0 -85
  131. package/src/lib/model/dataset-io.js.map +0 -1
  132. package/src/lib/model/index.js.map +0 -1
  133. package/src/lib/server/datasets-export-route.d.ts +0 -57
  134. package/src/lib/server/datasets-export-route.js +0 -229
  135. package/src/lib/server/datasets-export-route.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/data/src/lib/server/api-v1/datasets.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 checkDatasetQuota,\n checkEntitlement,\n checkQuota,\n coerceDocumentValues,\n createResourceUid,\n datasetIntegrityFields,\n datasetIntegrityUpdate,\n defaultScopeForNewResource,\n effectiveDatasetModel,\n newResourceScopeFields,\n validateDocument,\n} from '@aglyn/aglyn/server'\nimport {\n ApiErrors,\n apiJson,\n dataStorageRefusal,\n listResponse,\n} from '@aglyn/tenant-data-admin'\nimport {\n type ApiV1Context,\n claimWrite,\n paginate,\n readJsonBody,\n requireScope,\n serialize,\n} from '@aglyn/tenant-data-admin/server/api-v1-kit'\nimport { FieldValue, Timestamp } from 'firebase-admin/firestore'\nimport { announceDatasetRecords as announceDatasetChange } from '../announce-dataset-records'\nimport { loadCustomFieldTypes } from '../custom-field-types'\n\n/**\n * The organization's datasets on the customer REST API, `/v1/datasets/…`\n * (AGL-2126, AGL-3080): the datasets themselves and the records inside them.\n * Registered from this plugin's console server declarations; the console's\n * `/v1` router owns the pipeline in front — the key, the plan's API access,\n * the request quota, the rate limit — and hands every request under\n * `/v1/datasets` here.\n */\n\nconst datasetName = (data: FirebaseFirestore.DocumentData): string =>\n (data.displayName as string) ?? (data.name as string) ?? ''\n\nfunction datasetView(doc: FirebaseFirestore.DocumentSnapshot) {\n const data = doc.data() ?? {}\n return {\n id: doc.id,\n object: 'dataset',\n name: datasetName(data),\n fields: data.fields ?? [],\n created: serialize(data.createdAt) ?? null,\n }\n}\n\nfunction recordView(doc: FirebaseFirestore.DocumentSnapshot) {\n const data = doc.data() ?? {}\n return {\n id: doc.id,\n object: 'record',\n values: serialize(data.values ?? {}),\n created: serialize(data.createdAt) ?? null,\n updated: serialize(data.updatedAt) ?? null,\n }\n}\n\nfunction datasetsCollection(ctx: ApiV1Context) {\n return ctx.firestore.collection('orgs').doc(ctx.orgId).collection('datasets')\n}\n\n/** Serialized `model` ceiling, matching the console's create route. */\nconst DATASET_MODEL_MAX_BYTES = 64 * 1024\nconst DATASET_NAME_MAX = 120\nconst DATASET_FIELDS_MAX = 100\n\n/**\n * Validate the writable half of a dataset document (AGL-2126). Returns the\n * cleaned values, or the per-field map `conventions.md` publishes under\n * `validation_failed` — the same shape `validateDocument` produces for a\n * record, so a client branches on one thing across the whole resource.\n *\n * `partial` is what separates PATCH from POST: a create must be told a name\n * and at least one field, an update may send either alone. Absent keys are\n * left alone rather than cleared, because a PATCH that silently emptied\n * `fields` would take a dataset's schema away on a typo'd request body.\n */\nfunction readDatasetInput(\n body: Record<string, unknown>,\n { partial }: { partial: boolean },\n):\n | { values: { name?: string; fields?: string[]; model?: unknown } }\n | { errors: Record<string, string> } {\n const errors: Record<string, string> = {}\n const values: { name?: string; fields?: string[]; model?: unknown } = {}\n\n const hasName = body.name !== undefined\n if (hasName || !partial) {\n const name = String(body.name ?? '').trim().slice(0, DATASET_NAME_MAX)\n if (!name) errors.name = 'Required'\n else values.name = name\n }\n\n const hasFields = body.fields !== undefined\n if (hasFields || !partial) {\n if (!Array.isArray(body.fields)) {\n errors.fields = 'Must be an array of field names'\n } else {\n const fields = (body.fields as unknown[])\n .map((field) => String(field).trim())\n .filter((field) => field.length > 0)\n .slice(0, DATASET_FIELDS_MAX)\n if (fields.length === 0) errors.fields = 'At least one field is required'\n else values.fields = fields\n }\n }\n\n if (body.model !== undefined) {\n if (!body.model || typeof body.model !== 'object') {\n errors.model = 'Must be an object'\n } else if (JSON.stringify(body.model).length > DATASET_MODEL_MAX_BYTES) {\n errors.model = `Must serialize to under ${DATASET_MODEL_MAX_BYTES} bytes`\n } else {\n values.model = body.model\n }\n }\n\n return Object.keys(errors).length ? { errors } : { values }\n}\n\n/**\n * `POST /v1/datasets` (AGL-2126) — the call that lets the API bootstrap itself.\n *\n * Until this shipped, `/v1` could create and edit RECORDS but not the dataset\n * holding them, so an agency provisioning a client workspace, or a developer\n * keeping a schema in source control, had to click a dataset into existence\n * before their first API call could do anything. The console has been able to\n * do this all along (`apps/console/app/api/orgs/datasets`, `create-dataset`).\n *\n * The gates are the console route's, not a looser set. `dataStore` is the\n * entitlement, `checkDatasetQuota` is add-on aware, and — the one that is\n * invisible until it bites — `newResourceScopeFields` stamps `visibleTo`.\n * A dataset created without it matches no `array-contains-any` and therefore\n * renders on NO site at all (AGL-1044): the API would happily create data\n * that never appears anywhere, which is worse than refusing.\n */\nasync function createDataset(\n request: Request,\n ctx: ApiV1Context,\n): Promise<Response> {\n if (!checkEntitlement(ctx.org, 'dataStore')) {\n return ApiErrors.planRequired({\n message: 'Datasets are not included in this organization’s plan',\n code: 'data_store',\n headers: ctx.headers,\n })\n }\n\n const parsed = readDatasetInput(await readJsonBody(request), {\n partial: false,\n })\n if ('errors' in parsed) {\n return ApiErrors.badRequest({\n message: 'Dataset failed validation',\n code: 'validation_failed',\n fields: parsed.errors,\n headers: ctx.headers,\n })\n }\n\n /*\n * CLAIM ABOVE THE QUOTA (AGL-2296), and release on the refusal.\n *\n * This used to check the quota first and claim after, so that a refusal an\n * integrator can act on never consumed their key — a plan refusal is the\n * most retried failure there is, and it clears when somebody buys an\n * add-on. That reasoning is right and is preserved by the `release()`\n * below; the ORDERING it produced was not. A create that consumed the LAST\n * included slot could not be retried at all: the retry re-counts, is now AT\n * the band, and is refused before the claim is ever consulted — so the\n * replay never happens and the integrator cannot tell whether the dataset\n * exists. `conventions.md` publishes the opposite promise.\n *\n * Claiming first and releasing on each refusal gets both properties at\n * once, which is the trade `deleteRecord` already argues for and\n * `createContact` follows. Validation stays above: a deterministic 400 must\n * never take a key at all.\n */\n const collection = datasetsCollection(ctx)\n const claimed = await claimWrite(\n ctx,\n '*',\n request.headers.get('Idempotency-Key'),\n 'datasets',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const datasetCount = (await collection.count().get()).data().count\n const quota = checkDatasetQuota(ctx.org, datasetCount)\n if (!quota.allowed) {\n await claim.release()\n return ApiErrors.planRequired({\n message:\n `Dataset limit reached (${quota.limit}). ` +\n (quota.upgradeRequired\n ? 'Upgrade the plan to add more.'\n : `Buy extra datasets for $${quota.addonPriceUsd}/mo each, or upgrade.`),\n code: 'dataset_quota',\n headers: ctx.headers,\n })\n }\n\n const id = createResourceUid()\n await collection.doc(id).create({\n displayName: parsed.values.name,\n fields: parsed.values.fields,\n ...(parsed.values.model ? { model: parsed.values.model } : {}),\n // AGL-1484: the required argument exists so a creator that has not\n // decided cannot compile. No site is in context on an org-scoped API\n // key, so the org's own default is the only honest answer.\n ...newResourceScopeFields(\n defaultScopeForNewResource({\n defaultResourceScope: (\n ctx.org as { defaultResourceScope?: 'org' | 'host' }\n )?.defaultResourceScope,\n hostId: null,\n }),\n ),\n createdAt: Timestamp.now(),\n })\n const view = datasetView(await collection.doc(id).get())\n // Stored as 200 so a replay is distinguishable from the fresh 201 — the\n // rule `conventions.md` publishes and `createRecord` already follows.\n await claim.record(200, view)\n return apiJson(view, { status: 201, headers: ctx.headers })\n } catch (error) {\n // Same direction as every other v1 write: a stranded key is irreversible\n // from outside, a duplicate dataset is one DELETE away.\n await claim.release()\n throw error\n }\n}\n\n/**\n * `PATCH /v1/datasets/{id}` — rename, re-field, or re-model. Takes no\n * `Idempotency-Key` and does not need one, for `updateRecord`'s reason: the\n * same body twice lands the same state AND returns the same `200`.\n */\nasync function updateDataset(\n request: Request,\n ctx: ApiV1Context,\n datasetRef: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const snap = await datasetRef.get()\n if (!snap.exists) {\n return ApiErrors.notFound({ message: 'No such dataset', headers: ctx.headers })\n }\n const parsed = readDatasetInput(await readJsonBody(request), { partial: true })\n if ('errors' in parsed) {\n return ApiErrors.badRequest({\n message: 'Dataset failed validation',\n code: 'validation_failed',\n fields: parsed.errors,\n headers: ctx.headers,\n })\n }\n const { name, fields, model } = parsed.values\n const update: Record<string, unknown> = {}\n if (name !== undefined) update.displayName = name\n if (fields !== undefined) update.fields = fields\n if (model !== undefined) update.model = model\n // An empty body is a no-op, answered with the current dataset rather than a\n // 400: a client re-sending an unchanged object should not have to special-\n // case it, and there is no state to disagree about.\n if (Object.keys(update).length > 0) await datasetRef.update(update)\n // The fields and the model decide how every bound page formats and orders\n // this dataset's rows, so a schema change is a change to those pages — and\n // no publish follows a `/v1` write (AGL-3386). A rename alone renders\n // nothing: the display name is the console's label for the dataset. Best\n // effort, like the record writes' announce; the update is already stored.\n if (fields !== undefined || model !== undefined) {\n await announceDatasetChange({\n firestore: ctx.firestore,\n orgId: ctx.orgId,\n datasetId: datasetRef.id,\n })\n }\n return apiJson(datasetView(await datasetRef.get()), { headers: ctx.headers })\n}\n\n/**\n * `DELETE /v1/datasets/{id}` — refuses while records remain.\n *\n * A recursive delete is not something a single REST call should do quietly:\n * the records are the customer's content, and one mistyped id would take all\n * of them with no receipt naming what went. So this answers `409 conflict`\n * with the count, and the integrator deletes the records first — with the\n * same key semantics, through an endpoint that already exists.\n */\nasync function deleteDataset(\n request: Request,\n ctx: ApiV1Context,\n datasetRef: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const claimed = await claimWrite(\n ctx,\n datasetRef.id,\n request.headers.get('Idempotency-Key'),\n 'dataset-deletes',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const snap = await datasetRef.get()\n if (!snap.exists) {\n // Release: a wrong id is the integrator's to correct and retry with the\n // same key, exactly as `deleteRecord` does.\n await claim.release()\n return ApiErrors.notFound({ message: 'No such dataset', headers: ctx.headers })\n }\n const records = (await datasetRef.collection('records').count().get()).data()\n .count\n if (records > 0) {\n // Release too — this refusal clears once the records are gone, and the\n // retry that should then succeed must not replay the refusal.\n await claim.release()\n return ApiErrors.conflict({\n message: `Dataset still holds ${records} record${\n records === 1 ? '' : 's'\n }. Delete them first.`,\n code: 'dataset_not_empty',\n headers: ctx.headers,\n })\n }\n await datasetRef.delete()\n const view = { id: datasetRef.id, object: 'dataset', deleted: true }\n await claim.record(200, view)\n return apiJson(view, { headers: ctx.headers })\n } catch (error) {\n await claim.release()\n throw error\n }\n}\n\nexport async function handleDatasets(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const [, datasetId, sub, recordId] = segments\n\n // /v1/datasets\n if (!datasetId) {\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'datasets:read')\n if (denied) return denied\n const { docs, nextCursor } = await paginate(datasetsCollection(ctx), url)\n return listResponse(docs.map(datasetView), nextCursor, ctx.headers)\n }\n if (request.method === 'POST') {\n const denied = requireScope(ctx, 'datasets:write')\n if (denied) return denied\n return createDataset(request, ctx)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, POST' },\n })\n }\n\n const datasetRef = datasetsCollection(ctx).doc(datasetId)\n\n // /v1/datasets/{id}\n if (!sub) {\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'datasets:read')\n if (denied) return denied\n const snap = await datasetRef.get()\n if (!snap.exists) {\n return ApiErrors.notFound({ message: 'No such dataset', headers: ctx.headers })\n }\n return apiJson(datasetView(snap), { headers: ctx.headers })\n }\n if (request.method === 'PATCH') {\n const denied = requireScope(ctx, 'datasets:write')\n if (denied) return denied\n return updateDataset(request, ctx, datasetRef)\n }\n if (request.method === 'DELETE') {\n const denied = requireScope(ctx, 'datasets:write')\n if (denied) return denied\n return deleteDataset(request, ctx, datasetRef)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, PATCH, DELETE' },\n })\n }\n\n if (sub !== 'records') {\n return ApiErrors.notFound({ message: `Unknown endpoint`, headers: ctx.headers })\n }\n\n const datasetSnap = await datasetRef.get()\n if (!datasetSnap.exists) {\n return ApiErrors.notFound({ message: 'No such dataset', headers: ctx.headers })\n }\n const recordsRef = datasetRef.collection('records')\n\n // /v1/datasets/{id}/records\n if (!recordId) {\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'datasets:read')\n if (denied) return denied\n const { docs, nextCursor } = await paginate(recordsRef, url)\n return listResponse(docs.map(recordView), nextCursor, ctx.headers)\n }\n if (request.method === 'POST') {\n const denied = requireScope(ctx, 'datasets:write')\n if (denied) return denied\n return createRecord(request, ctx, datasetSnap, recordsRef)\n }\n return ApiErrors.methodNotAllowed({ headers: ctx.headers })\n }\n\n // /v1/datasets/{id}/records/{recordId}\n const recordRef = recordsRef.doc(recordId)\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'datasets:read')\n if (denied) return denied\n const snap = await recordRef.get()\n if (!snap.exists) return ApiErrors.notFound({ message: 'No such record', headers: ctx.headers })\n return apiJson(recordView(snap), { headers: ctx.headers })\n }\n if (request.method === 'PATCH') {\n const denied = requireScope(ctx, 'datasets:write')\n if (denied) return denied\n return updateRecord(request, ctx, datasetSnap, recordRef)\n }\n if (request.method === 'DELETE') {\n const denied = requireScope(ctx, 'datasets:write')\n if (denied) return denied\n return deleteRecord(request, ctx, datasetSnap.id, recordRef)\n }\n return ApiErrors.methodNotAllowed({ headers: ctx.headers })\n}\n\nasync function createRecord(\n request: Request,\n ctx: ApiV1Context,\n datasetSnap: FirebaseFirestore.DocumentSnapshot,\n recordsRef: FirebaseFirestore.CollectionReference,\n): Promise<Response> {\n const model = effectiveDatasetModel(datasetSnap.data() ?? {})\n // A plugin's field validator only runs once its plugin has registered it.\n await loadCustomFieldTypes(model, ctx.loadPluginSurfaces)\n const body = await readJsonBody(request)\n const coerced = coerceDocumentValues(model, (body.values as Record<string, unknown>) ?? {})\n const errors = validateDocument(model, coerced)\n if (Object.keys(errors).length) {\n return ApiErrors.badRequest({\n message: 'Record failed validation',\n headers: ctx.headers,\n code: 'validation_failed',\n // Name the offending fields (AGL-901): validateDocument already\n // produces this map, and a bare 'something is wrong' on a 20-field\n // record leaves an integrator bisecting their payload.\n fields: errors,\n })\n }\n\n /*\n * THE TWO QUOTAS THIS ROUTE DID NOT HAVE (AGL-2253).\n *\n * `POST /v1/datasets/{id}/records` counted rows only to compute `order` and\n * checked nothing: not `recordsPerDataset`, not `dataStorageMbPerOrg`. The\n * console route (`/api/orgs/datasets`) enforces both on the same write, and\n * the tenant form path enforces the rows half — so `/v1` was the one door\n * into `orgs/{id}/datasets/{id}/records` with no cap on it, and a customer\n * could blow a limit through the REST API that the UI refuses.\n *\n * They sit BELOW the idempotency claim (AGL-2296), which is a change from\n * how they shipped. Checking them first protected the key on a refusal —\n * right, and kept by the `release()` calls below — but it made a create\n * that consumed the LAST included row impossible to retry: the retry\n * re-counts, is now AT the band, and is refused before the claim is\n * consulted, so the replay never happens. The bytes leg is worse in\n * practice, because a bulk import crosses the storage band mid-run and\n * every retry from that point on is refused rather than replayed.\n *\n * The row count is reused for `order`, so this adds no read on the rows\n * leg. The bytes leg adds no read either on any plan that meters the\n * overage; see `dataStorageRefusal`.\n */\n // Idempotency: replay a prior create for the same key instead of\n // duplicating. Claimed HERE, below validation, so a deterministic 400 never\n // takes the key at all — an integrator fixes the payload and retries with\n // the same key, exactly as the POS cashier does (AGL-1691).\n const claimed = await claimWrite(\n ctx,\n datasetSnap.id,\n request.headers.get('Idempotency-Key'),\n 'records',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const order = (await recordsRef.count().get()).data().count\n const recordQuota = checkQuota(ctx.org, 'recordsPerDataset', order)\n if (!recordQuota.allowed) {\n await claim.release()\n return ApiErrors.planRequired({\n message: `Record limit reached (${recordQuota.limit}). Upgrade the plan to add more.`,\n code: 'record_quota',\n headers: ctx.headers,\n })\n }\n const storageRefusal = await dataStorageRefusal(\n ctx.org,\n ctx.firestore.collection('orgs').doc(ctx.orgId),\n )\n if (storageRefusal) {\n await claim.release()\n return ApiErrors.planRequired({\n message:\n storageRefusal.basis === 'always'\n ? 'Dataset storage is not included in this organization’s plan'\n : `Dataset storage limit reached (${storageRefusal.includedMb} MB). Upgrade the plan to add more.`,\n code: 'data_storage_quota',\n headers: ctx.headers,\n })\n }\n\n const recordId = createResourceUid()\n await recordsRef.doc(recordId).create({\n values: coerced,\n // The integrity index the delete check queries, written on the same\n // write as the values it describes.\n ...datasetIntegrityFields(model, coerced),\n order,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n })\n const created = await recordsRef.doc(recordId).get()\n const view = recordView(created)\n // AGL-2462 recorded that a `/v1` write cannot publish, and the ONLY thing\n // that made it visible was the hour. It announces now (AGL-3113): the\n // pages repeating over this dataset are dropped, on every site it is\n // shared with. Best effort — the record is already stored.\n await announceDatasetChange({\n firestore: ctx.firestore,\n orgId: ctx.orgId,\n datasetId: datasetSnap.id,\n })\n // Recorded as 200, never the 201 this answers: `conventions.md` publishes\n // the status as how a client tells a fresh create from a replay, and that\n // is the contract integrations branch on.\n await claim.record(200, view)\n return apiJson(view, { status: 201, headers: ctx.headers })\n } catch (error) {\n /*\n * Release on EVERY failure, including one whose outcome we cannot know —\n * the deliberate opposite of the refund (AGL-1696, `2dd52f01c`), which\n * strands the key on a throw. Same reasoning, applied where its money term\n * is absent.\n *\n * NOTHING on `/v1` moves money — the write surface is datasets, records,\n * the form-submission `read` flag and contacts (AGL-2276), while orders\n * and products stay read-only precisely because writing them would — so\n * \"a released key costs a second refund\" has no analogue here. The costs invert instead. A duplicate record is visible and\n * reversible BY THE INTEGRATOR with `DELETE /v1/datasets/{id}/records/{id}`\n * — same API, same scope, one extra call. A stranded key is irreversible\n * from outside: keys are documented as never expiring, and an integrator\n * derives them from their own upstream event ids precisely so retries\n * dedupe, so a stranded key means that event can never be written at all.\n *\n * Residual: a process killed between the claim and the record strands the\n * key regardless. `createdAtMs` is written so a sweeper can reap those.\n */\n await claim.release()\n throw error\n }\n}\n\nasync function updateRecord(\n request: Request,\n ctx: ApiV1Context,\n datasetSnap: FirebaseFirestore.DocumentSnapshot,\n recordRef: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const snap = await recordRef.get()\n if (!snap.exists) return ApiErrors.notFound({ message: 'No such record', headers: ctx.headers })\n\n const model = effectiveDatasetModel(datasetSnap.data() ?? {})\n // A plugin's field validator only runs once its plugin has registered it.\n await loadCustomFieldTypes(model, ctx.loadPluginSurfaces)\n const body = await readJsonBody(request)\n // PATCH merges the supplied fields over the stored values.\n const merged = {\n ...((snap.get('values') as Record<string, unknown>) ?? {}),\n ...coerceDocumentValues(model, (body.values as Record<string, unknown>) ?? {}),\n }\n const errors = validateDocument(model, merged)\n if (Object.keys(errors).length) {\n return ApiErrors.badRequest({\n message: 'Record failed validation',\n headers: ctx.headers,\n code: 'validation_failed',\n // Name the offending fields (AGL-901): validateDocument already\n // produces this map, and a bare 'something is wrong' on a 20-field\n // record leaves an integrator bisecting their payload.\n fields: errors,\n })\n }\n await recordRef.update({\n values: merged,\n // A PATCH that clears the last reference has to REMOVE the index, not\n // omit it: an update leaves an omitted field standing, and a stale\n // `referencedIds` refuses a delete nothing is holding.\n ...datasetIntegrityUpdate(model, merged, FieldValue.delete()),\n updatedAt: Timestamp.now(),\n })\n const updated = await recordRef.get()\n // An edited row is as stale on a live page as a new one (AGL-3113).\n await announceDatasetChange({\n firestore: ctx.firestore,\n orgId: ctx.orgId,\n datasetId: datasetSnap.id,\n })\n return apiJson(recordView(updated), { headers: ctx.headers })\n}\n\n/**\n * Delete a record, and let a retry of the SAME attempt say so (AGL-1710).\n *\n * This used to read, 404 if absent, then delete. The state after two calls was\n * right and the response was not: an integrator whose first response was lost\n * to a timeout retried, got `404 No such record`, and had no way to separate\n * \"already deleted, you're fine\" from \"that id was wrong and nothing was ever\n * deleted\". Both readings prescribe different actions and the wrong one —\n * escalate, or re-sync the whole dataset — is the one people pick.\n *\n * The obvious fix, `204` (or a `deleted: true` 200) for ANY missing record, is\n * rejected on two counts. It is a BREAKING change: `datasets.md` publishes\n * `404 not_found \"No such record\"` on this path and clients branch on it. And\n * it spends a signal to buy one — a caller that never saw the resource would\n * get a success for a typo'd id, losing the only feedback the API gives that\n * it is asking about the wrong thing. Consistency points the same way: `GET`\n * and `PATCH` on this very path answer 404 for a missing record, and every\n * sibling `DELETE` on `/v1` — the dataset, the form submission, the contact —\n * answers the same way, so there is no convention a 204 would be matching.\n *\n * So the key identifies the ATTEMPT rather than the resource, which is what\n * the shared claim is already for. A retry of the attempt that did the\n * deleting replays its receipt; a wrong id still 404s. That expands the\n * published contract instead of changing it — a caller sending no header sees\n * byte-identical behaviour.\n *\n * The claim is taken ABOVE the existence check, which is the one place this\n * deliberately diverges from `createRecord` (which claims below validation, so\n * a deterministic 400 never burns a key). It has to: the record a retry asks\n * about is precisely the one the first attempt removed, so a claim consulted\n * after the existence check would 404 the retry without ever reaching the\n * replay — the bug verbatim. The cost is taking-and-releasing on a genuine\n * miss, which is the trade the ordering requires.\n */\nasync function deleteRecord(\n request: Request,\n ctx: ApiV1Context,\n datasetId: string,\n recordRef: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const claimed = await claimWrite(\n ctx,\n datasetId,\n request.headers.get('Idempotency-Key'),\n 'record-deletes',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const snap = await recordRef.get()\n if (!snap.exists) {\n // Release, so the miss does not consume the key: the integrator corrects\n // the id and retries with the same one, exactly as a create's 400 lets\n // them correct the payload. A burned key here would be worse than the\n // 404 — keys never expire, and integrators derive them from upstream\n // event ids, so the corrected delete could never be sent at all.\n await claim.release()\n return ApiErrors.notFound({ message: 'No such record', headers: ctx.headers })\n }\n await recordRef.delete()\n // A row that is gone is the same staleness as a row that changed: the\n // page goes on rendering it until its cache is dropped (AGL-3113).\n await announceDatasetChange({\n firestore: ctx.firestore,\n orgId: ctx.orgId,\n datasetId,\n })\n const view = { id: recordRef.id, object: 'record', deleted: true }\n await claim.record(200, view)\n return apiJson(view, { headers: ctx.headers })\n } catch (error) {\n // Same direction as `createRecord`: a failed attempt gives the key back.\n // Nothing here moves money, and a stranded key is the irreversible failure.\n await claim.release()\n throw error\n }\n}\n"],"names":["checkDatasetQuota","checkEntitlement","checkQuota","coerceDocumentValues","createResourceUid","datasetIntegrityFields","datasetIntegrityUpdate","defaultScopeForNewResource","effectiveDatasetModel","newResourceScopeFields","validateDocument","ApiErrors","apiJson","dataStorageRefusal","listResponse","claimWrite","paginate","readJsonBody","requireScope","serialize","FieldValue","Timestamp","announceDatasetRecords","announceDatasetChange","loadCustomFieldTypes","datasetName","data","displayName","name","datasetView","doc","id","object","fields","created","createdAt","recordView","values","updated","updatedAt","datasetsCollection","ctx","firestore","collection","orgId","DATASET_MODEL_MAX_BYTES","DATASET_NAME_MAX","DATASET_FIELDS_MAX","readDatasetInput","body","partial","errors","hasName","undefined","String","trim","slice","hasFields","Array","isArray","map","field","filter","length","model","JSON","stringify","Object","keys","createDataset","request","org","planRequired","message","code","headers","parsed","badRequest","claimed","get","replay","claim","datasetCount","count","quota","allowed","release","limit","upgradeRequired","addonPriceUsd","create","defaultResourceScope","hostId","now","view","record","status","error","updateDataset","datasetRef","snap","exists","notFound","update","datasetId","deleteDataset","records","conflict","delete","deleted","handleDatasets","segments","url","sub","recordId","method","denied","docs","nextCursor","methodNotAllowed","Allow","datasetSnap","recordsRef","createRecord","recordRef","updateRecord","deleteRecord","loadPluginSurfaces","coerced","order","recordQuota","storageRefusal","basis","includedMb","merged"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/data/src/lib/server/api-v1/datasets.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 checkDatasetQuota,\n checkEntitlement,\n checkQuota,\n createResourceUid,\n defaultScopeForNewResource,\n newResourceScopeFields,\n} from '@aglyn/aglyn/server'\nimport {\n ApiErrors,\n apiJson,\n dataStorageRefusal,\n listResponse,\n} from '@aglyn/tenant-data-admin'\nimport {\n type ApiV1Context,\n claimWrite,\n paginate,\n readJsonBody,\n requireScope,\n serialize,\n} from '@aglyn/tenant-data-admin/server/api-v1-kit'\nimport { FieldValue, Timestamp } from 'firebase-admin/firestore'\nimport { announceDatasetRecords as announceDatasetChange } from '../announce-dataset-records'\nimport { loadCustomFieldTypes } from '../custom-field-types'\nimport { coerceDocumentValues, datasetIntegrityFields, datasetIntegrityUpdate, effectiveDatasetModel, validateDocument } from '../../model/dataset-models'\nimport { fillRecordAddresses } from '../../record-pages/record-pages'\n\n/**\n * The organization's datasets on the customer REST API, `/v1/datasets/…`\n * (AGL-2126, AGL-3080): the datasets themselves and the records inside them.\n * Registered from this plugin's console server declarations; the console's\n * `/v1` router owns the pipeline in front — the key, the plan's API access,\n * the request quota, the rate limit — and hands every request under\n * `/v1/datasets` here.\n */\n\nconst datasetName = (data: FirebaseFirestore.DocumentData): string =>\n (data.displayName as string) ?? (data.name as string) ?? ''\n\nfunction datasetView(doc: FirebaseFirestore.DocumentSnapshot) {\n const data = doc.data() ?? {}\n return {\n id: doc.id,\n object: 'dataset',\n name: datasetName(data),\n fields: data.fields ?? [],\n created: serialize(data.createdAt) ?? null,\n }\n}\n\nfunction recordView(doc: FirebaseFirestore.DocumentSnapshot) {\n const data = doc.data() ?? {}\n return {\n id: doc.id,\n object: 'record',\n values: serialize(data.values ?? {}),\n created: serialize(data.createdAt) ?? null,\n updated: serialize(data.updatedAt) ?? null,\n }\n}\n\nfunction datasetsCollection(ctx: ApiV1Context) {\n return ctx.firestore.collection('orgs').doc(ctx.orgId).collection('datasets')\n}\n\n/** Serialized `model` ceiling, matching the console's create route. */\nconst DATASET_MODEL_MAX_BYTES = 64 * 1024\nconst DATASET_NAME_MAX = 120\nconst DATASET_FIELDS_MAX = 100\n\n/**\n * Validate the writable half of a dataset document (AGL-2126). Returns the\n * cleaned values, or the per-field map `conventions.md` publishes under\n * `validation_failed` — the same shape `validateDocument` produces for a\n * record, so a client branches on one thing across the whole resource.\n *\n * `partial` is what separates PATCH from POST: a create must be told a name\n * and at least one field, an update may send either alone. Absent keys are\n * left alone rather than cleared, because a PATCH that silently emptied\n * `fields` would take a dataset's schema away on a typo'd request body.\n */\nfunction readDatasetInput(\n body: Record<string, unknown>,\n { partial }: { partial: boolean },\n):\n | { values: { name?: string; fields?: string[]; model?: unknown } }\n | { errors: Record<string, string> } {\n const errors: Record<string, string> = {}\n const values: { name?: string; fields?: string[]; model?: unknown } = {}\n\n const hasName = body.name !== undefined\n if (hasName || !partial) {\n const name = String(body.name ?? '').trim().slice(0, DATASET_NAME_MAX)\n if (!name) errors.name = 'Required'\n else values.name = name\n }\n\n const hasFields = body.fields !== undefined\n if (hasFields || !partial) {\n if (!Array.isArray(body.fields)) {\n errors.fields = 'Must be an array of field names'\n } else {\n const fields = (body.fields as unknown[])\n .map((field) => String(field).trim())\n .filter((field) => field.length > 0)\n .slice(0, DATASET_FIELDS_MAX)\n if (fields.length === 0) errors.fields = 'At least one field is required'\n else values.fields = fields\n }\n }\n\n if (body.model !== undefined) {\n if (!body.model || typeof body.model !== 'object') {\n errors.model = 'Must be an object'\n } else if (JSON.stringify(body.model).length > DATASET_MODEL_MAX_BYTES) {\n errors.model = `Must serialize to under ${DATASET_MODEL_MAX_BYTES} bytes`\n } else {\n values.model = body.model\n }\n }\n\n return Object.keys(errors).length ? { errors } : { values }\n}\n\n/**\n * `POST /v1/datasets` (AGL-2126) — the call that lets the API bootstrap itself.\n *\n * Until this shipped, `/v1` could create and edit RECORDS but not the dataset\n * holding them, so an agency provisioning a client workspace, or a developer\n * keeping a schema in source control, had to click a dataset into existence\n * before their first API call could do anything. The console has been able to\n * do this all along (`apps/console/app/api/orgs/datasets`, `create-dataset`).\n *\n * The gates are the console route's, not a looser set. `dataStore` is the\n * entitlement, `checkDatasetQuota` is add-on aware, and — the one that is\n * invisible until it bites — `newResourceScopeFields` stamps `visibleTo`.\n * A dataset created without it matches no `array-contains-any` and therefore\n * renders on NO site at all (AGL-1044): the API would happily create data\n * that never appears anywhere, which is worse than refusing.\n */\nasync function createDataset(\n request: Request,\n ctx: ApiV1Context,\n): Promise<Response> {\n if (!checkEntitlement(ctx.org, 'dataStore')) {\n return ApiErrors.planRequired({\n message: 'Datasets are not included in this organization’s plan',\n code: 'data_store',\n headers: ctx.headers,\n })\n }\n\n const parsed = readDatasetInput(await readJsonBody(request), {\n partial: false,\n })\n if ('errors' in parsed) {\n return ApiErrors.badRequest({\n message: 'Dataset failed validation',\n code: 'validation_failed',\n fields: parsed.errors,\n headers: ctx.headers,\n })\n }\n\n /*\n * CLAIM ABOVE THE QUOTA (AGL-2296), and release on the refusal.\n *\n * This used to check the quota first and claim after, so that a refusal an\n * integrator can act on never consumed their key — a plan refusal is the\n * most retried failure there is, and it clears when somebody buys an\n * add-on. That reasoning is right and is preserved by the `release()`\n * below; the ORDERING it produced was not. A create that consumed the LAST\n * included slot could not be retried at all: the retry re-counts, is now AT\n * the band, and is refused before the claim is ever consulted — so the\n * replay never happens and the integrator cannot tell whether the dataset\n * exists. `conventions.md` publishes the opposite promise.\n *\n * Claiming first and releasing on each refusal gets both properties at\n * once, which is the trade `deleteRecord` already argues for and\n * `createContact` follows. Validation stays above: a deterministic 400 must\n * never take a key at all.\n */\n const collection = datasetsCollection(ctx)\n const claimed = await claimWrite(\n ctx,\n '*',\n request.headers.get('Idempotency-Key'),\n 'datasets',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const datasetCount = (await collection.count().get()).data().count\n const quota = checkDatasetQuota(ctx.org, datasetCount)\n if (!quota.allowed) {\n await claim.release()\n return ApiErrors.planRequired({\n message:\n `Dataset limit reached (${quota.limit}). ` +\n (quota.upgradeRequired\n ? 'Upgrade the plan to add more.'\n : `Buy extra datasets for $${quota.addonPriceUsd}/mo each, or upgrade.`),\n code: 'dataset_quota',\n headers: ctx.headers,\n })\n }\n\n const id = createResourceUid()\n await collection.doc(id).create({\n displayName: parsed.values.name,\n fields: parsed.values.fields,\n ...(parsed.values.model ? { model: parsed.values.model } : {}),\n // AGL-1484: the required argument exists so a creator that has not\n // decided cannot compile. No site is in context on an org-scoped API\n // key, so the org's own default is the only honest answer.\n ...newResourceScopeFields(\n defaultScopeForNewResource({\n defaultResourceScope: (\n ctx.org as { defaultResourceScope?: 'org' | 'host' }\n )?.defaultResourceScope,\n hostId: null,\n }),\n ),\n createdAt: Timestamp.now(),\n })\n const view = datasetView(await collection.doc(id).get())\n // Stored as 200 so a replay is distinguishable from the fresh 201 — the\n // rule `conventions.md` publishes and `createRecord` already follows.\n await claim.record(200, view)\n return apiJson(view, { status: 201, headers: ctx.headers })\n } catch (error) {\n // Same direction as every other v1 write: a stranded key is irreversible\n // from outside, a duplicate dataset is one DELETE away.\n await claim.release()\n throw error\n }\n}\n\n/**\n * `PATCH /v1/datasets/{id}` — rename, re-field, or re-model. Takes no\n * `Idempotency-Key` and does not need one, for `updateRecord`'s reason: the\n * same body twice lands the same state AND returns the same `200`.\n */\nasync function updateDataset(\n request: Request,\n ctx: ApiV1Context,\n datasetRef: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const snap = await datasetRef.get()\n if (!snap.exists) {\n return ApiErrors.notFound({ message: 'No such dataset', headers: ctx.headers })\n }\n const parsed = readDatasetInput(await readJsonBody(request), { partial: true })\n if ('errors' in parsed) {\n return ApiErrors.badRequest({\n message: 'Dataset failed validation',\n code: 'validation_failed',\n fields: parsed.errors,\n headers: ctx.headers,\n })\n }\n const { name, fields, model } = parsed.values\n const update: Record<string, unknown> = {}\n if (name !== undefined) update.displayName = name\n if (fields !== undefined) update.fields = fields\n if (model !== undefined) update.model = model\n // An empty body is a no-op, answered with the current dataset rather than a\n // 400: a client re-sending an unchanged object should not have to special-\n // case it, and there is no state to disagree about.\n if (Object.keys(update).length > 0) await datasetRef.update(update)\n // The fields and the model decide how every bound page formats and orders\n // this dataset's rows, so a schema change is a change to those pages — and\n // no publish follows a `/v1` write (AGL-3386). A rename alone renders\n // nothing: the display name is the console's label for the dataset. Best\n // effort, like the record writes' announce; the update is already stored.\n if (fields !== undefined || model !== undefined) {\n await announceDatasetChange({\n firestore: ctx.firestore,\n orgId: ctx.orgId,\n datasetId: datasetRef.id,\n })\n }\n return apiJson(datasetView(await datasetRef.get()), { headers: ctx.headers })\n}\n\n/**\n * `DELETE /v1/datasets/{id}` — refuses while records remain.\n *\n * A recursive delete is not something a single REST call should do quietly:\n * the records are the customer's content, and one mistyped id would take all\n * of them with no receipt naming what went. So this answers `409 conflict`\n * with the count, and the integrator deletes the records first — with the\n * same key semantics, through an endpoint that already exists.\n */\nasync function deleteDataset(\n request: Request,\n ctx: ApiV1Context,\n datasetRef: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const claimed = await claimWrite(\n ctx,\n datasetRef.id,\n request.headers.get('Idempotency-Key'),\n 'dataset-deletes',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const snap = await datasetRef.get()\n if (!snap.exists) {\n // Release: a wrong id is the integrator's to correct and retry with the\n // same key, exactly as `deleteRecord` does.\n await claim.release()\n return ApiErrors.notFound({ message: 'No such dataset', headers: ctx.headers })\n }\n const records = (await datasetRef.collection('records').count().get()).data()\n .count\n if (records > 0) {\n // Release too — this refusal clears once the records are gone, and the\n // retry that should then succeed must not replay the refusal.\n await claim.release()\n return ApiErrors.conflict({\n message: `Dataset still holds ${records} record${\n records === 1 ? '' : 's'\n }. Delete them first.`,\n code: 'dataset_not_empty',\n headers: ctx.headers,\n })\n }\n await datasetRef.delete()\n const view = { id: datasetRef.id, object: 'dataset', deleted: true }\n await claim.record(200, view)\n return apiJson(view, { headers: ctx.headers })\n } catch (error) {\n await claim.release()\n throw error\n }\n}\n\nexport async function handleDatasets(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const [, datasetId, sub, recordId] = segments\n\n // /v1/datasets\n if (!datasetId) {\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'datasets:read')\n if (denied) return denied\n const { docs, nextCursor } = await paginate(datasetsCollection(ctx), url)\n return listResponse(docs.map(datasetView), nextCursor, ctx.headers)\n }\n if (request.method === 'POST') {\n const denied = requireScope(ctx, 'datasets:write')\n if (denied) return denied\n return createDataset(request, ctx)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, POST' },\n })\n }\n\n const datasetRef = datasetsCollection(ctx).doc(datasetId)\n\n // /v1/datasets/{id}\n if (!sub) {\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'datasets:read')\n if (denied) return denied\n const snap = await datasetRef.get()\n if (!snap.exists) {\n return ApiErrors.notFound({ message: 'No such dataset', headers: ctx.headers })\n }\n return apiJson(datasetView(snap), { headers: ctx.headers })\n }\n if (request.method === 'PATCH') {\n const denied = requireScope(ctx, 'datasets:write')\n if (denied) return denied\n return updateDataset(request, ctx, datasetRef)\n }\n if (request.method === 'DELETE') {\n const denied = requireScope(ctx, 'datasets:write')\n if (denied) return denied\n return deleteDataset(request, ctx, datasetRef)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, PATCH, DELETE' },\n })\n }\n\n if (sub !== 'records') {\n return ApiErrors.notFound({ message: `Unknown endpoint`, headers: ctx.headers })\n }\n\n const datasetSnap = await datasetRef.get()\n if (!datasetSnap.exists) {\n return ApiErrors.notFound({ message: 'No such dataset', headers: ctx.headers })\n }\n const recordsRef = datasetRef.collection('records')\n\n // /v1/datasets/{id}/records\n if (!recordId) {\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'datasets:read')\n if (denied) return denied\n const { docs, nextCursor } = await paginate(recordsRef, url)\n return listResponse(docs.map(recordView), nextCursor, ctx.headers)\n }\n if (request.method === 'POST') {\n const denied = requireScope(ctx, 'datasets:write')\n if (denied) return denied\n return createRecord(request, ctx, datasetSnap, recordsRef)\n }\n return ApiErrors.methodNotAllowed({ headers: ctx.headers })\n }\n\n // /v1/datasets/{id}/records/{recordId}\n const recordRef = recordsRef.doc(recordId)\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'datasets:read')\n if (denied) return denied\n const snap = await recordRef.get()\n if (!snap.exists) return ApiErrors.notFound({ message: 'No such record', headers: ctx.headers })\n return apiJson(recordView(snap), { headers: ctx.headers })\n }\n if (request.method === 'PATCH') {\n const denied = requireScope(ctx, 'datasets:write')\n if (denied) return denied\n return updateRecord(request, ctx, datasetSnap, recordRef)\n }\n if (request.method === 'DELETE') {\n const denied = requireScope(ctx, 'datasets:write')\n if (denied) return denied\n return deleteRecord(request, ctx, datasetSnap.id, recordRef)\n }\n return ApiErrors.methodNotAllowed({ headers: ctx.headers })\n}\n\nasync function createRecord(\n request: Request,\n ctx: ApiV1Context,\n datasetSnap: FirebaseFirestore.DocumentSnapshot,\n recordsRef: FirebaseFirestore.CollectionReference,\n): Promise<Response> {\n const model = effectiveDatasetModel(datasetSnap.data() ?? {})\n // A plugin's field validator only runs once its plugin has registered it.\n await loadCustomFieldTypes(model, ctx.loadPluginSurfaces)\n const body = await readJsonBody(request)\n const coerced = fillRecordAddresses(\n model,\n coerceDocumentValues(model, (body.values as Record<string, unknown>) ?? {}),\n )\n const errors = validateDocument(model, coerced)\n if (Object.keys(errors).length) {\n return ApiErrors.badRequest({\n message: 'Record failed validation',\n headers: ctx.headers,\n code: 'validation_failed',\n // Name the offending fields (AGL-901): validateDocument already\n // produces this map, and a bare 'something is wrong' on a 20-field\n // record leaves an integrator bisecting their payload.\n fields: errors,\n })\n }\n\n /*\n * THE TWO QUOTAS THIS ROUTE DID NOT HAVE (AGL-2253).\n *\n * `POST /v1/datasets/{id}/records` counted rows only to compute `order` and\n * checked nothing: not `recordsPerDataset`, not `dataStorageMbPerOrg`. The\n * console route (`/api/orgs/datasets`) enforces both on the same write, and\n * the tenant form path enforces the rows half — so `/v1` was the one door\n * into `orgs/{id}/datasets/{id}/records` with no cap on it, and a customer\n * could blow a limit through the REST API that the UI refuses.\n *\n * They sit BELOW the idempotency claim (AGL-2296), which is a change from\n * how they shipped. Checking them first protected the key on a refusal —\n * right, and kept by the `release()` calls below — but it made a create\n * that consumed the LAST included row impossible to retry: the retry\n * re-counts, is now AT the band, and is refused before the claim is\n * consulted, so the replay never happens. The bytes leg is worse in\n * practice, because a bulk import crosses the storage band mid-run and\n * every retry from that point on is refused rather than replayed.\n *\n * The row count is reused for `order`, so this adds no read on the rows\n * leg. The bytes leg adds no read either on any plan that meters the\n * overage; see `dataStorageRefusal`.\n */\n // Idempotency: replay a prior create for the same key instead of\n // duplicating. Claimed HERE, below validation, so a deterministic 400 never\n // takes the key at all — an integrator fixes the payload and retries with\n // the same key, exactly as the POS cashier does (AGL-1691).\n const claimed = await claimWrite(\n ctx,\n datasetSnap.id,\n request.headers.get('Idempotency-Key'),\n 'records',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const order = (await recordsRef.count().get()).data().count\n const recordQuota = checkQuota(ctx.org, 'recordsPerDataset', order)\n if (!recordQuota.allowed) {\n await claim.release()\n return ApiErrors.planRequired({\n message: `Record limit reached (${recordQuota.limit}). Upgrade the plan to add more.`,\n code: 'record_quota',\n headers: ctx.headers,\n })\n }\n const storageRefusal = await dataStorageRefusal(\n ctx.org,\n ctx.firestore.collection('orgs').doc(ctx.orgId),\n )\n if (storageRefusal) {\n await claim.release()\n return ApiErrors.planRequired({\n message:\n storageRefusal.basis === 'always'\n ? 'Dataset storage is not included in this organization’s plan'\n : `Dataset storage limit reached (${storageRefusal.includedMb} MB). Upgrade the plan to add more.`,\n code: 'data_storage_quota',\n headers: ctx.headers,\n })\n }\n\n const recordId = createResourceUid()\n await recordsRef.doc(recordId).create({\n values: coerced,\n // The integrity index the delete check queries, written on the same\n // write as the values it describes.\n ...datasetIntegrityFields(model, coerced),\n order,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n })\n const created = await recordsRef.doc(recordId).get()\n const view = recordView(created)\n // AGL-2462 recorded that a `/v1` write cannot publish, and the ONLY thing\n // that made it visible was the hour. It announces now (AGL-3113): the\n // pages repeating over this dataset are dropped, on every site it is\n // shared with. Best effort — the record is already stored.\n await announceDatasetChange({\n firestore: ctx.firestore,\n orgId: ctx.orgId,\n datasetId: datasetSnap.id,\n })\n // Recorded as 200, never the 201 this answers: `conventions.md` publishes\n // the status as how a client tells a fresh create from a replay, and that\n // is the contract integrations branch on.\n await claim.record(200, view)\n return apiJson(view, { status: 201, headers: ctx.headers })\n } catch (error) {\n /*\n * Release on EVERY failure, including one whose outcome we cannot know —\n * the deliberate opposite of the refund (AGL-1696, `2dd52f01c`), which\n * strands the key on a throw. Same reasoning, applied where its money term\n * is absent.\n *\n * NOTHING on `/v1` moves money — the write surface is datasets, records,\n * the form-submission `read` flag and contacts (AGL-2276), while orders\n * and products stay read-only precisely because writing them would — so\n * \"a released key costs a second refund\" has no analogue here. The costs invert instead. A duplicate record is visible and\n * reversible BY THE INTEGRATOR with `DELETE /v1/datasets/{id}/records/{id}`\n * — same API, same scope, one extra call. A stranded key is irreversible\n * from outside: keys are documented as never expiring, and an integrator\n * derives them from their own upstream event ids precisely so retries\n * dedupe, so a stranded key means that event can never be written at all.\n *\n * Residual: a process killed between the claim and the record strands the\n * key regardless. `createdAtMs` is written so a sweeper can reap those.\n */\n await claim.release()\n throw error\n }\n}\n\nasync function updateRecord(\n request: Request,\n ctx: ApiV1Context,\n datasetSnap: FirebaseFirestore.DocumentSnapshot,\n recordRef: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const snap = await recordRef.get()\n if (!snap.exists) return ApiErrors.notFound({ message: 'No such record', headers: ctx.headers })\n\n const model = effectiveDatasetModel(datasetSnap.data() ?? {})\n // A plugin's field validator only runs once its plugin has registered it.\n await loadCustomFieldTypes(model, ctx.loadPluginSurfaces)\n const body = await readJsonBody(request)\n // PATCH merges the supplied fields over the stored values.\n const merged = fillRecordAddresses(model, {\n ...((snap.get('values') as Record<string, unknown>) ?? {}),\n ...coerceDocumentValues(model, (body.values as Record<string, unknown>) ?? {}),\n })\n const errors = validateDocument(model, merged)\n if (Object.keys(errors).length) {\n return ApiErrors.badRequest({\n message: 'Record failed validation',\n headers: ctx.headers,\n code: 'validation_failed',\n // Name the offending fields (AGL-901): validateDocument already\n // produces this map, and a bare 'something is wrong' on a 20-field\n // record leaves an integrator bisecting their payload.\n fields: errors,\n })\n }\n await recordRef.update({\n values: merged,\n // A PATCH that clears the last reference has to REMOVE the index, not\n // omit it: an update leaves an omitted field standing, and a stale\n // `referencedIds` refuses a delete nothing is holding.\n ...datasetIntegrityUpdate(model, merged, FieldValue.delete()),\n updatedAt: Timestamp.now(),\n })\n const updated = await recordRef.get()\n // An edited row is as stale on a live page as a new one (AGL-3113).\n await announceDatasetChange({\n firestore: ctx.firestore,\n orgId: ctx.orgId,\n datasetId: datasetSnap.id,\n })\n return apiJson(recordView(updated), { headers: ctx.headers })\n}\n\n/**\n * Delete a record, and let a retry of the SAME attempt say so (AGL-1710).\n *\n * This used to read, 404 if absent, then delete. The state after two calls was\n * right and the response was not: an integrator whose first response was lost\n * to a timeout retried, got `404 No such record`, and had no way to separate\n * \"already deleted, you're fine\" from \"that id was wrong and nothing was ever\n * deleted\". Both readings prescribe different actions and the wrong one —\n * escalate, or re-sync the whole dataset — is the one people pick.\n *\n * The obvious fix, `204` (or a `deleted: true` 200) for ANY missing record, is\n * rejected on two counts. It is a BREAKING change: `datasets.md` publishes\n * `404 not_found \"No such record\"` on this path and clients branch on it. And\n * it spends a signal to buy one — a caller that never saw the resource would\n * get a success for a typo'd id, losing the only feedback the API gives that\n * it is asking about the wrong thing. Consistency points the same way: `GET`\n * and `PATCH` on this very path answer 404 for a missing record, and every\n * sibling `DELETE` on `/v1` — the dataset, the form submission, the contact —\n * answers the same way, so there is no convention a 204 would be matching.\n *\n * So the key identifies the ATTEMPT rather than the resource, which is what\n * the shared claim is already for. A retry of the attempt that did the\n * deleting replays its receipt; a wrong id still 404s. That expands the\n * published contract instead of changing it — a caller sending no header sees\n * byte-identical behaviour.\n *\n * The claim is taken ABOVE the existence check, which is the one place this\n * deliberately diverges from `createRecord` (which claims below validation, so\n * a deterministic 400 never burns a key). It has to: the record a retry asks\n * about is precisely the one the first attempt removed, so a claim consulted\n * after the existence check would 404 the retry without ever reaching the\n * replay — the bug verbatim. The cost is taking-and-releasing on a genuine\n * miss, which is the trade the ordering requires.\n */\nasync function deleteRecord(\n request: Request,\n ctx: ApiV1Context,\n datasetId: string,\n recordRef: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const claimed = await claimWrite(\n ctx,\n datasetId,\n request.headers.get('Idempotency-Key'),\n 'record-deletes',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const snap = await recordRef.get()\n if (!snap.exists) {\n // Release, so the miss does not consume the key: the integrator corrects\n // the id and retries with the same one, exactly as a create's 400 lets\n // them correct the payload. A burned key here would be worse than the\n // 404 — keys never expire, and integrators derive them from upstream\n // event ids, so the corrected delete could never be sent at all.\n await claim.release()\n return ApiErrors.notFound({ message: 'No such record', headers: ctx.headers })\n }\n await recordRef.delete()\n // A row that is gone is the same staleness as a row that changed: the\n // page goes on rendering it until its cache is dropped (AGL-3113).\n await announceDatasetChange({\n firestore: ctx.firestore,\n orgId: ctx.orgId,\n datasetId,\n })\n const view = { id: recordRef.id, object: 'record', deleted: true }\n await claim.record(200, view)\n return apiJson(view, { headers: ctx.headers })\n } catch (error) {\n // Same direction as `createRecord`: a failed attempt gives the key back.\n // Nothing here moves money, and a stranded key is the irreversible failure.\n await claim.release()\n throw error\n }\n}\n"],"names":["checkDatasetQuota","checkEntitlement","checkQuota","createResourceUid","defaultScopeForNewResource","newResourceScopeFields","ApiErrors","apiJson","dataStorageRefusal","listResponse","claimWrite","paginate","readJsonBody","requireScope","serialize","FieldValue","Timestamp","announceDatasetRecords","announceDatasetChange","loadCustomFieldTypes","coerceDocumentValues","datasetIntegrityFields","datasetIntegrityUpdate","effectiveDatasetModel","validateDocument","fillRecordAddresses","datasetName","data","displayName","name","datasetView","doc","id","object","fields","created","createdAt","recordView","values","updated","updatedAt","datasetsCollection","ctx","firestore","collection","orgId","DATASET_MODEL_MAX_BYTES","DATASET_NAME_MAX","DATASET_FIELDS_MAX","readDatasetInput","body","partial","errors","hasName","undefined","String","trim","slice","hasFields","Array","isArray","map","field","filter","length","model","JSON","stringify","Object","keys","createDataset","request","org","planRequired","message","code","headers","parsed","badRequest","claimed","get","replay","claim","datasetCount","count","quota","allowed","release","limit","upgradeRequired","addonPriceUsd","create","defaultResourceScope","hostId","now","view","record","status","error","updateDataset","datasetRef","snap","exists","notFound","update","datasetId","deleteDataset","records","conflict","delete","deleted","handleDatasets","segments","url","sub","recordId","method","denied","docs","nextCursor","methodNotAllowed","Allow","datasetSnap","recordsRef","createRecord","recordRef","updateRecord","deleteRecord","loadPluginSurfaces","coerced","order","recordQuota","storageRefusal","basis","includedMb","merged"],"mappings":"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"}
@@ -14,11 +14,11 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { effectiveDatasetModel } from "@aglyn/aglyn/app-utils/dataset-models";
18
- import { hostRoleFor, isOrgWideMember, memberCanSee } from "@aglyn/aglyn/app-utils/organizations";
17
+ */ import { hostRoleFor, isOrgWideMember, memberCanSee } from "@aglyn/aglyn/app-utils/organizations";
19
18
  import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
20
19
  import { PLUGIN_FIGURE_MAX_ROWS, registerPluginFigureReader } from "@aglyn/aglyn/plugin-manager/plugin-figures";
21
20
  import { BUNDLE_ID } from "../constants/bundle-common.js";
21
+ import { effectiveDatasetModel } from "../model/dataset-models.js";
22
22
  /** Datasets a dataset summary reads. */ export const DATASET_FIGURE_DATASETS_READ_LIMIT = 50;
23
23
  /** Records a dataset breakdown or field summary reads, in document order; the rest are named in a note. */ export const DATASET_FIGURE_RECORDS_READ_LIMIT = 2000;
24
24
  /** The fewest records a breakdown shows a group of by its own label. */ export const DATASET_FIGURE_MIN_GROUP = 3;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/dataset-figures.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 { effectiveDatasetModel, type DatasetModel } from '@aglyn/aglyn/app-utils/dataset-models'\nimport { hostRoleFor, isOrgWideMember, memberCanSee } from '@aglyn/aglyn/app-utils/organizations'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport {\n PLUGIN_FIGURE_MAX_ROWS,\n registerPluginFigureReader,\n type PluginFigureRead,\n type PluginFigureReader,\n type PluginFigureRequest,\n type PluginFigureRow,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * The figures an insight reads about the workspace's datasets (AGL-2915),\n * registered by the plugin that keeps them on the `plugin-figures` seam\n * (AGL-3080), so the asking plugin reads no dataset itself.\n *\n * Each reader answers with one compact table of aggregates, read-only and\n * about nobody: counts, sums and rates, labeled by a dataset's own field —\n * never a record. A breakdown refuses a field with more than\n * `DATASET_FIGURE_MAX_GROUP_VALUES` different values — on a dataset of any\n * size, a name, an address or an order number — and folds every group of\n * fewer than `DATASET_FIGURE_MIN_GROUP` records into one row, so each label it\n * shows is a value at least that many records share.\n *\n * A request reads only the datasets the member who asked may see — and,\n * narrowed to a site, only those shared with it, which is the site Data\n * page's rule (AGL-2891). With no member, only datasets shared with every\n * site.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** How a reader reaches Firestore: resolved when it reads, never when it registers. */\nexport type DatasetFigureFirestore = () => Firestore\n\n/** Datasets a dataset summary reads. */\nexport const DATASET_FIGURE_DATASETS_READ_LIMIT = 50\n\n/** Records a dataset breakdown or field summary reads, in document order; the rest are named in a note. */\nexport const DATASET_FIGURE_RECORDS_READ_LIMIT = 2_000\n\n/** The fewest records a breakdown shows a group of by its own label. */\nexport const DATASET_FIGURE_MIN_GROUP = 3\n\n/**\n * The most different values a breakdown groups by: every state and territory,\n * a status, a category. A field with more tells records apart rather than\n * sorting them, so it is refused.\n */\nexport const DATASET_FIGURE_MAX_GROUP_VALUES = 60\n\n/** The aggregates a breakdown computes. */\nexport const DATASET_FIGURE_OPERATIONS = ['count', 'sum', 'average', 'min', 'max'] as const\nexport type DatasetFigureOperation = (typeof DATASET_FIGURE_OPERATIONS)[number]\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nconst num = (value: unknown): number => {\n const parsed = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nconst round1 = (value: number): number => Math.round(value * 10) / 10\n\nconst share = (part: number, whole: number): number | null => (whole > 0 ? round1((part / whole) * 100) : null)\n\nfunction refused(status: 400 | 403 | 404, error: string): PluginFigureRead {\n return { ok: false, status, error }\n}\n\ninterface VisibleDataset {\n id: string\n name: string\n model: DatasetModel\n}\n\n/**\n * The datasets a request may read: those the member who asked may see — and,\n * narrowed to a site, only those shared with it, which is the site Data page's\n * rule (AGL-2891). With no member, only datasets shared with every site.\n */\nasync function visibleDatasets(firestore: Firestore, request: PluginFigureRequest): Promise<VisibleDataset[]> {\n const orgRef = firestore.collection('orgs').doc(request.orgId)\n const [snapshot, memberSnapshot] = await Promise.all([\n orgRef.collection('datasets').limit(DATASET_FIGURE_DATASETS_READ_LIMIT).get(),\n request.uid ? orgRef.collection('members').doc(request.uid).get() : Promise.resolve(null),\n ])\n const member = memberSnapshot?.exists ? (memberSnapshot.data() as Partial<AglynOrgMember>) : null\n if (request.uid && !member) return []\n if (request.hostId && member && !isOrgWideMember(member) && hostRoleFor(member, request.hostId) === null) {\n return []\n }\n return snapshot.docs\n .map((doc) => ({ id: doc.id, data: (doc.data() ?? {}) as Record<string, unknown> }))\n .filter(({ data }) => {\n const visibleTo = data['visibleTo'] as string[] | undefined\n if (data['deletedAt']) return false\n if (request.hostId && !visibleToHost(visibleTo, request.hostId)) return false\n return member ? memberCanSee(member, visibleTo) : Boolean(visibleTo?.includes('org'))\n })\n .map(({ id, data }) => ({\n id,\n name: str(data['displayName']) || id,\n model: effectiveDatasetModel(data as { model?: DatasetModel; fields?: string[] }),\n }))\n .sort((a, b) => a.name.localeCompare(b.name))\n}\n\nasync function readRecords(\n firestore: Firestore,\n orgId: string,\n datasetId: string,\n): Promise<{ rows: Array<Record<string, unknown>>; total: number }> {\n const recordsRef = firestore.collection('orgs').doc(orgId).collection('datasets').doc(datasetId).collection('records')\n const [page, count] = await Promise.all([\n recordsRef.limit(DATASET_FIGURE_RECORDS_READ_LIMIT).get(),\n recordsRef.count().get(),\n ])\n return {\n rows: page.docs.map((doc) => ((doc.data() ?? {})['values'] ?? {}) as Record<string, unknown>),\n total: num(count.data().count),\n }\n}\n\nconst NUMERIC_TYPES = new Set(['float', 'int32', 'int64'])\n\nfunction readNote(read: number, total: number): string[] {\n return total > read\n ? [`Read the first ${read.toLocaleString('en-US')} of ${total.toLocaleString('en-US')} records, so every figure covers only those.`]\n : []\n}\n\n/** A field a dataset table may be read by: the model's own id, or its name as the dataset page shows it. */\nfunction fieldOf(model: DatasetModel, raw: string): string | null {\n const wanted = raw.trim().toLowerCase()\n for (const id of model.order) {\n if (id.toLowerCase() === wanted || str(model.fields[id]?.name).toLowerCase() === wanted) return id\n }\n return null\n}\n\n/** A value as a breakdown groups it: text as written, a number or a yes/no as text; empty is its own group. */\nfunction groupLabel(value: unknown): string {\n if (value === null || value === undefined || value === '') return '(empty)'\n if (typeof value === 'boolean') return value ? 'Yes' : 'No'\n if (typeof value === 'number' || typeof value === 'string') return String(value).trim() || '(empty)'\n return '(not a single value)'\n}\n\nexport function datasetFigureReaders(firestore: DatasetFigureFirestore): PluginFigureReader[] {\n const base = { scope: 'org' as const, plugin: 'data', feature: 'dataStore', windows: [] as number[] }\n // Datasets belong to the workspace, so a table links its Data page.\n const source = { label: 'Data', path: 'data' }\n return [\n {\n ...base,\n id: 'datasets.summary',\n label: 'Datasets',\n description:\n 'With no dataset named: each dataset’s records and fields. With one: each of its fields, how many records fill it, how many distinct values it holds and, for a number, its lowest, average and highest.',\n params: [{ name: 'dataset', description: 'A dataset’s id or name, to summarize its fields.', required: false }],\n read: async (request) => {\n const db = firestore()\n const datasets = await visibleDatasets(db, request)\n const named = request.params['dataset']\n if (!named) {\n const counted = await Promise.all(\n datasets.slice(0, PLUGIN_FIGURE_MAX_ROWS).map(async (dataset) => ({\n dataset: dataset.name,\n records: num(\n (\n await db\n .collection('orgs')\n .doc(request.orgId)\n .collection('datasets')\n .doc(dataset.id)\n .collection('records')\n .count()\n .get()\n ).data().count,\n ),\n fields: dataset.model.order.length,\n })),\n )\n return {\n ok: true,\n table: {\n title: 'Datasets',\n source,\n period: null,\n columns: [\n { key: 'dataset', label: 'Dataset', kind: 'text' },\n { key: 'records', label: 'Records', kind: 'count' },\n { key: 'fields', label: 'Fields', kind: 'count' },\n ],\n rows: counted,\n omitted: Math.max(0, datasets.length - counted.length),\n notes: [],\n },\n }\n }\n const dataset = datasets.find((entry) => entry.id === named || entry.name.toLowerCase() === named.toLowerCase())\n if (!dataset) return refused(404, 'That dataset is not one you can read here')\n const { rows: values, total } = await readRecords(db, request.orgId, dataset.id)\n const rows = dataset.model.order.slice(0, PLUGIN_FIGURE_MAX_ROWS).map((fieldId) => {\n const field = dataset.model.fields[fieldId]\n const filled = values.filter((entry) => entry[fieldId] !== null && entry[fieldId] !== undefined && entry[fieldId] !== '')\n const numbers = NUMERIC_TYPES.has(String(field?.type))\n ? filled.map((entry) => entry[fieldId]).filter((value): value is number => typeof value === 'number' && Number.isFinite(value))\n : []\n return {\n field: str(field?.name) || fieldId,\n type: String(field?.type ?? 'text'),\n filled: share(filled.length, values.length),\n distinct: new Set(filled.map((entry) => groupLabel(entry[fieldId]))).size,\n lowest: numbers.length ? Math.min(...numbers) : null,\n average: numbers.length ? Math.round((numbers.reduce((sum, value) => sum + value, 0) / numbers.length) * 100) / 100 : null,\n highest: numbers.length ? Math.max(...numbers) : null,\n }\n })\n return {\n ok: true,\n table: {\n title: `${dataset.name}: fields`,\n source,\n period: null,\n columns: [\n { key: 'field', label: 'Field', kind: 'text' },\n { key: 'type', label: 'Type', kind: 'text' },\n { key: 'filled', label: 'Records that fill it', kind: 'percent' },\n { key: 'distinct', label: 'Distinct values', kind: 'count' },\n { key: 'lowest', label: 'Lowest', kind: 'number' },\n { key: 'average', label: 'Average', kind: 'number' },\n { key: 'highest', label: 'Highest', kind: 'number' },\n ],\n rows,\n omitted: Math.max(0, dataset.model.order.length - rows.length),\n notes: readNote(values.length, total),\n },\n }\n },\n },\n {\n ...base,\n id: 'datasets.breakdown',\n label: 'Dataset breakdown',\n description:\n 'One dataset’s records grouped by the values of one field, with how many records each group holds and, for a number field, its sum, average, lowest or highest in each group.',\n params: [\n { name: 'dataset', description: 'The dataset’s id or name.', required: true },\n { name: 'group', description: 'The field to group records by, by id or name.', required: true },\n { name: 'measure', description: 'A number field to aggregate, by id or name; empty to count records.', required: false },\n { name: 'operation', description: `One of ${DATASET_FIGURE_OPERATIONS.join(', ')}; count when empty.`, required: false },\n ],\n read: async (request) => {\n const db = firestore()\n const datasets = await visibleDatasets(db, request)\n const named = request.params['dataset'] ?? ''\n const dataset = datasets.find((entry) => entry.id === named || entry.name.toLowerCase() === named.toLowerCase())\n if (!dataset) return refused(404, 'That dataset is not one you can read here')\n const group = fieldOf(dataset.model, request.params['group'] ?? '')\n if (!group) return refused(400, 'That dataset has no field by that name to group by')\n const operation = (request.params['operation'] ?? 'count') as DatasetFigureOperation\n if (!(DATASET_FIGURE_OPERATIONS as readonly string[]).includes(operation)) {\n return refused(400, `The operation is one of ${DATASET_FIGURE_OPERATIONS.join(', ')}`)\n }\n const measure = request.params['measure'] ? fieldOf(dataset.model, request.params['measure']) : null\n if (operation !== 'count' && (!measure || !NUMERIC_TYPES.has(String(dataset.model.fields[measure]?.type)))) {\n return refused(400, 'Only a number field can be summed, averaged or ranged')\n }\n const { rows: values, total } = await readRecords(db, request.orgId, dataset.id)\n const groups = new Map<string, number[]>()\n const sizes = new Map<string, number>()\n for (const entry of values) {\n const label = groupLabel(entry[group])\n sizes.set(label, (sizes.get(label) ?? 0) + 1)\n const value = measure ? entry[measure] : null\n if (typeof value === 'number' && Number.isFinite(value)) {\n groups.set(label, [...(groups.get(label) ?? []), value])\n }\n }\n if (sizes.size > DATASET_FIGURE_MAX_GROUP_VALUES) {\n return refused(\n 400,\n `That field has more than ${DATASET_FIGURE_MAX_GROUP_VALUES} different values, too many to group by. Group by a field many records share, such as a state, a status or a category`,\n )\n }\n // A group of fewer than DATASET_FIGURE_MIN_GROUP records is folded into\n // one row: a group that small is a record, and its label could be a\n // person's name.\n const small = [...sizes.entries()].filter(([, size]) => size < DATASET_FIGURE_MIN_GROUP)\n const aggregate = (numbers: number[]): number | null => {\n if (operation === 'count') return null\n if (!numbers.length) return null\n const sum = numbers.reduce((acc, value) => acc + value, 0)\n switch (operation) {\n case 'sum':\n return Math.round(sum * 100) / 100\n case 'average':\n return Math.round((sum / numbers.length) * 100) / 100\n case 'min':\n return Math.min(...numbers)\n default:\n return Math.max(...numbers)\n }\n }\n const row = (group: string, records: number, numbers: number[]): PluginFigureRow =>\n operation === 'count' ? { group, records } : { group, records, value: aggregate(numbers) }\n const rows: PluginFigureRow[] = [...sizes.entries()]\n .filter(([, size]) => size >= DATASET_FIGURE_MIN_GROUP)\n .map(([label, size]) => row(label, size, groups.get(label) ?? []))\n if (small.length) {\n rows.push(\n row(\n `Other (${small.length} groups under ${DATASET_FIGURE_MIN_GROUP} records)`,\n small.reduce((sum, [, size]) => sum + size, 0),\n small.flatMap(([label]) => groups.get(label) ?? []),\n ),\n )\n }\n rows.sort((a, b) => (num(b['value'] ?? b['records']) - num(a['value'] ?? a['records'])))\n const groupName = str(dataset.model.fields[group]?.name) || group\n const measureName = measure ? str(dataset.model.fields[measure]?.name) || measure : ''\n return {\n ok: true,\n table: {\n title: `${dataset.name} by ${groupName}`,\n source,\n period: null,\n columns: [\n { key: 'group', label: groupName, kind: 'text' },\n { key: 'records', label: 'Records', kind: 'count' },\n ...(operation === 'count'\n ? []\n : [{ key: 'value', label: `${operation[0].toUpperCase()}${operation.slice(1)} of ${measureName}`, kind: 'number' as const }]),\n ],\n rows: rows.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, rows.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: [\n ...readNote(values.length, total),\n ...(small.length ? [`Groups of fewer than ${DATASET_FIGURE_MIN_GROUP} records are counted together as Other.`] : []),\n ],\n },\n }\n },\n },\n ]\n}\n\n/**\n * Registers them from the console's server declarations: the console is the\n * one surface that runs insight jobs. Registering again replaces this\n * plugin's own readers.\n */\nexport function registerDatasetFigureReaders(firestore: DatasetFigureFirestore): void {\n for (const reader of datasetFigureReaders(firestore)) {\n registerPluginFigureReader(reader, { pluginId: BUNDLE_ID })\n }\n}\n"],"names":["effectiveDatasetModel","hostRoleFor","isOrgWideMember","memberCanSee","visibleToHost","PLUGIN_FIGURE_MAX_ROWS","registerPluginFigureReader","BUNDLE_ID","DATASET_FIGURE_DATASETS_READ_LIMIT","DATASET_FIGURE_RECORDS_READ_LIMIT","DATASET_FIGURE_MIN_GROUP","DATASET_FIGURE_MAX_GROUP_VALUES","DATASET_FIGURE_OPERATIONS","str","value","trim","num","parsed","Number","isFinite","round1","Math","round","share","part","whole","refused","status","error","ok","visibleDatasets","firestore","request","orgRef","collection","doc","orgId","snapshot","memberSnapshot","Promise","all","limit","get","uid","resolve","member","exists","data","hostId","docs","map","id","filter","visibleTo","Boolean","includes","name","model","sort","a","b","localeCompare","readRecords","datasetId","recordsRef","page","count","rows","total","NUMERIC_TYPES","Set","readNote","read","toLocaleString","fieldOf","raw","wanted","toLowerCase","order","fields","groupLabel","undefined","String","datasetFigureReaders","base","scope","plugin","feature","windows","source","label","path","description","params","required","db","datasets","named","counted","slice","dataset","records","length","table","title","period","columns","key","kind","omitted","max","notes","find","entry","values","fieldId","field","filled","numbers","has","type","distinct","size","lowest","min","average","reduce","sum","highest","join","group","operation","measure","groups","Map","sizes","set","small","entries","aggregate","acc","row","push","flatMap","groupName","measureName","toUpperCase","registerDatasetFigureReaders","reader","pluginId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/dataset-figures.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 { hostRoleFor, isOrgWideMember, memberCanSee } from '@aglyn/aglyn/app-utils/organizations'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport {\n PLUGIN_FIGURE_MAX_ROWS,\n registerPluginFigureReader,\n type PluginFigureRead,\n type PluginFigureReader,\n type PluginFigureRequest,\n type PluginFigureRow,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { type DatasetModel, effectiveDatasetModel } from '../model/dataset-models'\n\n/**\n * The figures an insight reads about the workspace's datasets (AGL-2915),\n * registered by the plugin that keeps them on the `plugin-figures` seam\n * (AGL-3080), so the asking plugin reads no dataset itself.\n *\n * Each reader answers with one compact table of aggregates, read-only and\n * about nobody: counts, sums and rates, labeled by a dataset's own field —\n * never a record. A breakdown refuses a field with more than\n * `DATASET_FIGURE_MAX_GROUP_VALUES` different values — on a dataset of any\n * size, a name, an address or an order number — and folds every group of\n * fewer than `DATASET_FIGURE_MIN_GROUP` records into one row, so each label it\n * shows is a value at least that many records share.\n *\n * A request reads only the datasets the member who asked may see — and,\n * narrowed to a site, only those shared with it, which is the site Data\n * page's rule (AGL-2891). With no member, only datasets shared with every\n * site.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** How a reader reaches Firestore: resolved when it reads, never when it registers. */\nexport type DatasetFigureFirestore = () => Firestore\n\n/** Datasets a dataset summary reads. */\nexport const DATASET_FIGURE_DATASETS_READ_LIMIT = 50\n\n/** Records a dataset breakdown or field summary reads, in document order; the rest are named in a note. */\nexport const DATASET_FIGURE_RECORDS_READ_LIMIT = 2_000\n\n/** The fewest records a breakdown shows a group of by its own label. */\nexport const DATASET_FIGURE_MIN_GROUP = 3\n\n/**\n * The most different values a breakdown groups by: every state and territory,\n * a status, a category. A field with more tells records apart rather than\n * sorting them, so it is refused.\n */\nexport const DATASET_FIGURE_MAX_GROUP_VALUES = 60\n\n/** The aggregates a breakdown computes. */\nexport const DATASET_FIGURE_OPERATIONS = ['count', 'sum', 'average', 'min', 'max'] as const\nexport type DatasetFigureOperation = (typeof DATASET_FIGURE_OPERATIONS)[number]\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nconst num = (value: unknown): number => {\n const parsed = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nconst round1 = (value: number): number => Math.round(value * 10) / 10\n\nconst share = (part: number, whole: number): number | null => (whole > 0 ? round1((part / whole) * 100) : null)\n\nfunction refused(status: 400 | 403 | 404, error: string): PluginFigureRead {\n return { ok: false, status, error }\n}\n\ninterface VisibleDataset {\n id: string\n name: string\n model: DatasetModel\n}\n\n/**\n * The datasets a request may read: those the member who asked may see — and,\n * narrowed to a site, only those shared with it, which is the site Data page's\n * rule (AGL-2891). With no member, only datasets shared with every site.\n */\nasync function visibleDatasets(firestore: Firestore, request: PluginFigureRequest): Promise<VisibleDataset[]> {\n const orgRef = firestore.collection('orgs').doc(request.orgId)\n const [snapshot, memberSnapshot] = await Promise.all([\n orgRef.collection('datasets').limit(DATASET_FIGURE_DATASETS_READ_LIMIT).get(),\n request.uid ? orgRef.collection('members').doc(request.uid).get() : Promise.resolve(null),\n ])\n const member = memberSnapshot?.exists ? (memberSnapshot.data() as Partial<AglynOrgMember>) : null\n if (request.uid && !member) return []\n if (request.hostId && member && !isOrgWideMember(member) && hostRoleFor(member, request.hostId) === null) {\n return []\n }\n return snapshot.docs\n .map((doc) => ({ id: doc.id, data: (doc.data() ?? {}) as Record<string, unknown> }))\n .filter(({ data }) => {\n const visibleTo = data['visibleTo'] as string[] | undefined\n if (data['deletedAt']) return false\n if (request.hostId && !visibleToHost(visibleTo, request.hostId)) return false\n return member ? memberCanSee(member, visibleTo) : Boolean(visibleTo?.includes('org'))\n })\n .map(({ id, data }) => ({\n id,\n name: str(data['displayName']) || id,\n model: effectiveDatasetModel(data as { model?: DatasetModel; fields?: string[] }),\n }))\n .sort((a, b) => a.name.localeCompare(b.name))\n}\n\nasync function readRecords(\n firestore: Firestore,\n orgId: string,\n datasetId: string,\n): Promise<{ rows: Array<Record<string, unknown>>; total: number }> {\n const recordsRef = firestore.collection('orgs').doc(orgId).collection('datasets').doc(datasetId).collection('records')\n const [page, count] = await Promise.all([\n recordsRef.limit(DATASET_FIGURE_RECORDS_READ_LIMIT).get(),\n recordsRef.count().get(),\n ])\n return {\n rows: page.docs.map((doc) => ((doc.data() ?? {})['values'] ?? {}) as Record<string, unknown>),\n total: num(count.data().count),\n }\n}\n\nconst NUMERIC_TYPES = new Set(['float', 'int32', 'int64'])\n\nfunction readNote(read: number, total: number): string[] {\n return total > read\n ? [`Read the first ${read.toLocaleString('en-US')} of ${total.toLocaleString('en-US')} records, so every figure covers only those.`]\n : []\n}\n\n/** A field a dataset table may be read by: the model's own id, or its name as the dataset page shows it. */\nfunction fieldOf(model: DatasetModel, raw: string): string | null {\n const wanted = raw.trim().toLowerCase()\n for (const id of model.order) {\n if (id.toLowerCase() === wanted || str(model.fields[id]?.name).toLowerCase() === wanted) return id\n }\n return null\n}\n\n/** A value as a breakdown groups it: text as written, a number or a yes/no as text; empty is its own group. */\nfunction groupLabel(value: unknown): string {\n if (value === null || value === undefined || value === '') return '(empty)'\n if (typeof value === 'boolean') return value ? 'Yes' : 'No'\n if (typeof value === 'number' || typeof value === 'string') return String(value).trim() || '(empty)'\n return '(not a single value)'\n}\n\nexport function datasetFigureReaders(firestore: DatasetFigureFirestore): PluginFigureReader[] {\n const base = { scope: 'org' as const, plugin: 'data', feature: 'dataStore', windows: [] as number[] }\n // Datasets belong to the workspace, so a table links its Data page.\n const source = { label: 'Data', path: 'data' }\n return [\n {\n ...base,\n id: 'datasets.summary',\n label: 'Datasets',\n description:\n 'With no dataset named: each dataset’s records and fields. With one: each of its fields, how many records fill it, how many distinct values it holds and, for a number, its lowest, average and highest.',\n params: [{ name: 'dataset', description: 'A dataset’s id or name, to summarize its fields.', required: false }],\n read: async (request) => {\n const db = firestore()\n const datasets = await visibleDatasets(db, request)\n const named = request.params['dataset']\n if (!named) {\n const counted = await Promise.all(\n datasets.slice(0, PLUGIN_FIGURE_MAX_ROWS).map(async (dataset) => ({\n dataset: dataset.name,\n records: num(\n (\n await db\n .collection('orgs')\n .doc(request.orgId)\n .collection('datasets')\n .doc(dataset.id)\n .collection('records')\n .count()\n .get()\n ).data().count,\n ),\n fields: dataset.model.order.length,\n })),\n )\n return {\n ok: true,\n table: {\n title: 'Datasets',\n source,\n period: null,\n columns: [\n { key: 'dataset', label: 'Dataset', kind: 'text' },\n { key: 'records', label: 'Records', kind: 'count' },\n { key: 'fields', label: 'Fields', kind: 'count' },\n ],\n rows: counted,\n omitted: Math.max(0, datasets.length - counted.length),\n notes: [],\n },\n }\n }\n const dataset = datasets.find((entry) => entry.id === named || entry.name.toLowerCase() === named.toLowerCase())\n if (!dataset) return refused(404, 'That dataset is not one you can read here')\n const { rows: values, total } = await readRecords(db, request.orgId, dataset.id)\n const rows = dataset.model.order.slice(0, PLUGIN_FIGURE_MAX_ROWS).map((fieldId) => {\n const field = dataset.model.fields[fieldId]\n const filled = values.filter((entry) => entry[fieldId] !== null && entry[fieldId] !== undefined && entry[fieldId] !== '')\n const numbers = NUMERIC_TYPES.has(String(field?.type))\n ? filled.map((entry) => entry[fieldId]).filter((value): value is number => typeof value === 'number' && Number.isFinite(value))\n : []\n return {\n field: str(field?.name) || fieldId,\n type: String(field?.type ?? 'text'),\n filled: share(filled.length, values.length),\n distinct: new Set(filled.map((entry) => groupLabel(entry[fieldId]))).size,\n lowest: numbers.length ? Math.min(...numbers) : null,\n average: numbers.length ? Math.round((numbers.reduce((sum, value) => sum + value, 0) / numbers.length) * 100) / 100 : null,\n highest: numbers.length ? Math.max(...numbers) : null,\n }\n })\n return {\n ok: true,\n table: {\n title: `${dataset.name}: fields`,\n source,\n period: null,\n columns: [\n { key: 'field', label: 'Field', kind: 'text' },\n { key: 'type', label: 'Type', kind: 'text' },\n { key: 'filled', label: 'Records that fill it', kind: 'percent' },\n { key: 'distinct', label: 'Distinct values', kind: 'count' },\n { key: 'lowest', label: 'Lowest', kind: 'number' },\n { key: 'average', label: 'Average', kind: 'number' },\n { key: 'highest', label: 'Highest', kind: 'number' },\n ],\n rows,\n omitted: Math.max(0, dataset.model.order.length - rows.length),\n notes: readNote(values.length, total),\n },\n }\n },\n },\n {\n ...base,\n id: 'datasets.breakdown',\n label: 'Dataset breakdown',\n description:\n 'One dataset’s records grouped by the values of one field, with how many records each group holds and, for a number field, its sum, average, lowest or highest in each group.',\n params: [\n { name: 'dataset', description: 'The dataset’s id or name.', required: true },\n { name: 'group', description: 'The field to group records by, by id or name.', required: true },\n { name: 'measure', description: 'A number field to aggregate, by id or name; empty to count records.', required: false },\n { name: 'operation', description: `One of ${DATASET_FIGURE_OPERATIONS.join(', ')}; count when empty.`, required: false },\n ],\n read: async (request) => {\n const db = firestore()\n const datasets = await visibleDatasets(db, request)\n const named = request.params['dataset'] ?? ''\n const dataset = datasets.find((entry) => entry.id === named || entry.name.toLowerCase() === named.toLowerCase())\n if (!dataset) return refused(404, 'That dataset is not one you can read here')\n const group = fieldOf(dataset.model, request.params['group'] ?? '')\n if (!group) return refused(400, 'That dataset has no field by that name to group by')\n const operation = (request.params['operation'] ?? 'count') as DatasetFigureOperation\n if (!(DATASET_FIGURE_OPERATIONS as readonly string[]).includes(operation)) {\n return refused(400, `The operation is one of ${DATASET_FIGURE_OPERATIONS.join(', ')}`)\n }\n const measure = request.params['measure'] ? fieldOf(dataset.model, request.params['measure']) : null\n if (operation !== 'count' && (!measure || !NUMERIC_TYPES.has(String(dataset.model.fields[measure]?.type)))) {\n return refused(400, 'Only a number field can be summed, averaged or ranged')\n }\n const { rows: values, total } = await readRecords(db, request.orgId, dataset.id)\n const groups = new Map<string, number[]>()\n const sizes = new Map<string, number>()\n for (const entry of values) {\n const label = groupLabel(entry[group])\n sizes.set(label, (sizes.get(label) ?? 0) + 1)\n const value = measure ? entry[measure] : null\n if (typeof value === 'number' && Number.isFinite(value)) {\n groups.set(label, [...(groups.get(label) ?? []), value])\n }\n }\n if (sizes.size > DATASET_FIGURE_MAX_GROUP_VALUES) {\n return refused(\n 400,\n `That field has more than ${DATASET_FIGURE_MAX_GROUP_VALUES} different values, too many to group by. Group by a field many records share, such as a state, a status or a category`,\n )\n }\n // A group of fewer than DATASET_FIGURE_MIN_GROUP records is folded into\n // one row: a group that small is a record, and its label could be a\n // person's name.\n const small = [...sizes.entries()].filter(([, size]) => size < DATASET_FIGURE_MIN_GROUP)\n const aggregate = (numbers: number[]): number | null => {\n if (operation === 'count') return null\n if (!numbers.length) return null\n const sum = numbers.reduce((acc, value) => acc + value, 0)\n switch (operation) {\n case 'sum':\n return Math.round(sum * 100) / 100\n case 'average':\n return Math.round((sum / numbers.length) * 100) / 100\n case 'min':\n return Math.min(...numbers)\n default:\n return Math.max(...numbers)\n }\n }\n const row = (group: string, records: number, numbers: number[]): PluginFigureRow =>\n operation === 'count' ? { group, records } : { group, records, value: aggregate(numbers) }\n const rows: PluginFigureRow[] = [...sizes.entries()]\n .filter(([, size]) => size >= DATASET_FIGURE_MIN_GROUP)\n .map(([label, size]) => row(label, size, groups.get(label) ?? []))\n if (small.length) {\n rows.push(\n row(\n `Other (${small.length} groups under ${DATASET_FIGURE_MIN_GROUP} records)`,\n small.reduce((sum, [, size]) => sum + size, 0),\n small.flatMap(([label]) => groups.get(label) ?? []),\n ),\n )\n }\n rows.sort((a, b) => (num(b['value'] ?? b['records']) - num(a['value'] ?? a['records'])))\n const groupName = str(dataset.model.fields[group]?.name) || group\n const measureName = measure ? str(dataset.model.fields[measure]?.name) || measure : ''\n return {\n ok: true,\n table: {\n title: `${dataset.name} by ${groupName}`,\n source,\n period: null,\n columns: [\n { key: 'group', label: groupName, kind: 'text' },\n { key: 'records', label: 'Records', kind: 'count' },\n ...(operation === 'count'\n ? []\n : [{ key: 'value', label: `${operation[0].toUpperCase()}${operation.slice(1)} of ${measureName}`, kind: 'number' as const }]),\n ],\n rows: rows.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, rows.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: [\n ...readNote(values.length, total),\n ...(small.length ? [`Groups of fewer than ${DATASET_FIGURE_MIN_GROUP} records are counted together as Other.`] : []),\n ],\n },\n }\n },\n },\n ]\n}\n\n/**\n * Registers them from the console's server declarations: the console is the\n * one surface that runs insight jobs. Registering again replaces this\n * plugin's own readers.\n */\nexport function registerDatasetFigureReaders(firestore: DatasetFigureFirestore): void {\n for (const reader of datasetFigureReaders(firestore)) {\n registerPluginFigureReader(reader, { pluginId: BUNDLE_ID })\n }\n}\n"],"names":["hostRoleFor","isOrgWideMember","memberCanSee","visibleToHost","PLUGIN_FIGURE_MAX_ROWS","registerPluginFigureReader","BUNDLE_ID","effectiveDatasetModel","DATASET_FIGURE_DATASETS_READ_LIMIT","DATASET_FIGURE_RECORDS_READ_LIMIT","DATASET_FIGURE_MIN_GROUP","DATASET_FIGURE_MAX_GROUP_VALUES","DATASET_FIGURE_OPERATIONS","str","value","trim","num","parsed","Number","isFinite","round1","Math","round","share","part","whole","refused","status","error","ok","visibleDatasets","firestore","request","orgRef","collection","doc","orgId","snapshot","memberSnapshot","Promise","all","limit","get","uid","resolve","member","exists","data","hostId","docs","map","id","filter","visibleTo","Boolean","includes","name","model","sort","a","b","localeCompare","readRecords","datasetId","recordsRef","page","count","rows","total","NUMERIC_TYPES","Set","readNote","read","toLocaleString","fieldOf","raw","wanted","toLowerCase","order","fields","groupLabel","undefined","String","datasetFigureReaders","base","scope","plugin","feature","windows","source","label","path","description","params","required","db","datasets","named","counted","slice","dataset","records","length","table","title","period","columns","key","kind","omitted","max","notes","find","entry","values","fieldId","field","filled","numbers","has","type","distinct","size","lowest","min","average","reduce","sum","highest","join","group","operation","measure","groups","Map","sizes","set","small","entries","aggregate","acc","row","push","flatMap","groupName","measureName","toUpperCase","registerDatasetFigureReaders","reader","pluginId"],"mappings":"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"}
@@ -0,0 +1,147 @@
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 { Firestore } from 'firebase-admin/firestore';
18
+ import { type LivePageDropper, type LivePageTarget } from '@aglyn/tenant-data-admin/server/live-page-drops';
19
+ import { type UsageSources } from '@aglyn/tenant-data-admin/server/live-page-usage';
20
+ /**
21
+ * How many documents per collection the per-site scan reads.
22
+ *
23
+ * The console's publish scan uses the same number for the same reason
24
+ * (AGL-1161): this decides which caches get dropped, so a prefix scan would
25
+ * report a refreshed site and leave real pages serving the old rows. Bounded
26
+ * all the same, and when the bound bites it is SAID rather than absorbed.
27
+ */
28
+ export declare const DATASET_SCAN_LIMIT = 2000;
29
+ /**
30
+ * How many sites one record write fans out to.
31
+ *
32
+ * `MAX_SCOPE_HOSTS` is the ceiling on a NAMED selection, so only an org-wide
33
+ * dataset can reach this — an org with more sites than this pays a scan per
34
+ * site on every write otherwise, which is a cost the person writing one row
35
+ * did not ask for. Sites past the cap keep the TTL they had before this
36
+ * existed, and the shortfall is returned and logged rather than swallowed.
37
+ */
38
+ export declare const MAX_DATASET_FANOUT_HOSTS = 30;
39
+ /**
40
+ * How long one site-and-dataset pair waits before announcing again.
41
+ *
42
+ * A 1,000-row import is one announce because the importer announces after its
43
+ * loop, not per row. This covers the other burst — many small writes arriving
44
+ * separately, a form under load or an automation fanning out — where each one
45
+ * would otherwise pay a full scan per site and hand the tenant the same path
46
+ * list again.
47
+ *
48
+ * Leading edge, so the FIRST write of a burst refreshes immediately; what is
49
+ * skipped is a repeat inside the window, and the next write after it announces
50
+ * again. The cost of the skip is bounded by the window: a row written during
51
+ * one is visible no later than the next announce.
52
+ */
53
+ export declare const DATASET_ANNOUNCE_WINDOW_MS = 2000;
54
+ /** Test seam: the window is process-wide state, so a spec has to reset it. */
55
+ export declare function resetDatasetAnnounceThrottle(): void;
56
+ export interface DatasetLivePageScope {
57
+ targets: LivePageTarget[];
58
+ /**
59
+ * Sites the fan-out cap left out. Zero in the ordinary case, and reported
60
+ * rather than logged because a caller that says "refreshed" while some sites
61
+ * were skipped is the failure this whole arc exists to remove.
62
+ */
63
+ hostsDropped: number;
64
+ /**
65
+ * Why there is nothing to announce, or `'ok'`.
66
+ *
67
+ * `'not-rendered'` is a success: a dataset no page repeats over has no live
68
+ * page to refresh. It is kept apart from `'error'` for the same reason the
69
+ * revalidate route keeps `not-routed` apart from a refusal — one means there
70
+ * was nothing to do, the other means something was left stale.
71
+ */
72
+ reason: 'ok' | 'no-dataset' | 'no-sites' | 'not-rendered' | 'error';
73
+ }
74
+ /**
75
+ * Every live screen whose rendered output repeats over the dataset, however
76
+ * indirectly.
77
+ *
78
+ * The same three-bucket closure a placed form's pages are found by, for the
79
+ * same reason: a binding is found by SEARCHING node trees rather than by
80
+ * matching a pointer, so it can sit on the screen, in the chrome of the layout
81
+ * the screen renders inside, or inside a reusable component a third component
82
+ * nests. After the first level the fan-out is identical, so it is delegated
83
+ * rather than restated.
84
+ *
85
+ * Pure, and separate from the Firestore read, so the closure is testable
86
+ * without a database.
87
+ */
88
+ export declare function screenIdsRepeatingDataset(keys: ReadonlySet<string>, sources: UsageSources): string[];
89
+ /**
90
+ * Every live page a change to this dataset's records makes stale, per site.
91
+ *
92
+ * Reads only; announcing is the caller's, because the two runtimes announce
93
+ * differently — the console posts to the tenant over the shared secret, and
94
+ * the tenant drops its own caches in process. Splitting it here is what lets
95
+ * both use one answer to the question of WHICH pages.
96
+ *
97
+ * Never throws. A scope that could not be computed is a scope of nothing, and
98
+ * the TTL underneath is unaffected.
99
+ */
100
+ export declare function datasetLivePageScope(options: {
101
+ firestore: Firestore;
102
+ orgId: string;
103
+ datasetId: string;
104
+ }): Promise<DatasetLivePageScope>;
105
+ export interface DatasetAnnounceResult {
106
+ /** Sites whose cached pages were dropped. */
107
+ sitesRefreshed: number;
108
+ /** Pages named across those sites. */
109
+ pathsRefreshed: number;
110
+ /** Sites the drop was attempted on and refused. */
111
+ sitesRefused: number;
112
+ /** Sites past the fan-out cap, and sites whose scan was truncated. */
113
+ hostsDropped: number;
114
+ scanTruncated: boolean;
115
+ /** `'throttled'` when a drop for this pair already went out just now. */
116
+ reason: DatasetLivePageScope['reason'] | 'throttled' | 'no-dropper';
117
+ }
118
+ /**
119
+ * Refresh every live page that shows this dataset's records.
120
+ *
121
+ * The one call a write path makes. BEST EFFORT, ALWAYS: it never throws and
122
+ * never rejects, so a caller can `await` it inside the try that already guards
123
+ * the write without any chance of turning a stored record into a failed one.
124
+ *
125
+ * `drop` overrides the registered dropper for callers that hold their own —
126
+ * the console, whose announce is an authenticated request rather than a local
127
+ * cache call.
128
+ */
129
+ export declare function announceDatasetRecordChange(options: {
130
+ firestore: Firestore;
131
+ orgId: string;
132
+ datasetId: string;
133
+ drop?: LivePageDropper;
134
+ }): Promise<DatasetAnnounceResult>;
135
+ /**
136
+ * One sentence for a refresh that did not cover every page, or `null` when it
137
+ * did.
138
+ *
139
+ * Beside the call rather than at each surface, so the console cannot word this
140
+ * differently in two places — the same reason `describeRevalidateShortfall`
141
+ * lives beside `revalidateLivePages`. `lead` names the act that succeeded,
142
+ * because only that word differs between surfaces: a record is SAVED, and
143
+ * telling someone who fixed a typo that they published it is a worse error
144
+ * than the staleness the sentence is about.
145
+ */
146
+ export declare function describeDatasetAnnounceShortfall(result: DatasetAnnounceResult | null, lead?: string): string | null;
147
+ export default announceDatasetRecordChange;