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

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 (63) hide show
  1. package/README.md +1 -1
  2. package/package.json +10 -10
  3. package/src/lib/constants/bundle-common.d.ts +2 -0
  4. package/src/lib/constants/bundle-common.js +1 -0
  5. package/src/lib/constants/bundle-common.js.map +1 -1
  6. package/src/lib/declarations.console-server.d.ts +47 -0
  7. package/src/lib/declarations.console-server.js +74 -0
  8. package/src/lib/declarations.console-server.js.map +1 -0
  9. package/src/lib/declarations.server.d.ts +4 -0
  10. package/src/lib/declarations.server.js +11 -1
  11. package/src/lib/declarations.server.js.map +1 -1
  12. package/src/lib/model/dataset-csv.d.ts +36 -0
  13. package/src/lib/model/dataset-csv.js +85 -0
  14. package/src/lib/model/dataset-csv.js.map +1 -0
  15. package/src/lib/model/dataset-io.d.ts +8 -7
  16. package/src/lib/model/dataset-io.js +9 -8
  17. package/src/lib/model/dataset-io.js.map +1 -1
  18. package/src/lib/model/dataset-record-list.d.ts +40 -0
  19. package/src/lib/model/dataset-record-list.js +61 -0
  20. package/src/lib/model/dataset-record-list.js.map +1 -0
  21. package/src/lib/model/dataset-record.d.ts +33 -0
  22. package/src/lib/model/dataset-record.js +68 -0
  23. package/src/lib/model/dataset-record.js.map +1 -0
  24. package/src/lib/plugin.js +3 -0
  25. package/src/lib/plugin.js.map +1 -1
  26. package/src/lib/server/announce-dataset-records.d.ts +37 -0
  27. package/src/lib/server/announce-dataset-records.js +54 -0
  28. package/src/lib/server/announce-dataset-records.js.map +1 -0
  29. package/src/lib/server/api-v1/datasets.d.ts +18 -0
  30. package/src/lib/server/api-v1/datasets.js +646 -0
  31. package/src/lib/server/api-v1/datasets.js.map +1 -0
  32. package/src/lib/server/api-v1/openapi.d.ts +26 -0
  33. package/src/lib/server/api-v1/openapi.js +260 -0
  34. package/src/lib/server/api-v1/openapi.js.map +1 -0
  35. package/src/lib/server/api-v1/usage.d.ts +26 -0
  36. package/src/lib/server/api-v1/usage.js +33 -0
  37. package/src/lib/server/api-v1/usage.js.map +1 -0
  38. package/src/lib/server/custom-field-types.d.ts +39 -0
  39. package/src/lib/server/custom-field-types.js +41 -0
  40. package/src/lib/server/custom-field-types.js.map +1 -0
  41. package/src/lib/server/dataset-figures.d.ts +61 -0
  42. package/src/lib/server/dataset-figures.js +413 -0
  43. package/src/lib/server/dataset-figures.js.map +1 -0
  44. package/src/lib/server/dataset-record-index.d.ts +18 -0
  45. package/src/lib/server/dataset-record-index.js +62 -0
  46. package/src/lib/server/dataset-record-index.js.map +1 -0
  47. package/src/lib/server/dataset-storage-meter.d.ts +29 -0
  48. package/src/lib/server/dataset-storage-meter.js +82 -0
  49. package/src/lib/server/dataset-storage-meter.js.map +1 -0
  50. package/src/lib/server/datasets-export-route.d.ts +57 -0
  51. package/src/lib/server/datasets-export-route.js +229 -0
  52. package/src/lib/server/datasets-export-route.js.map +1 -0
  53. package/src/lib/server/datasets-route.d.ts +38 -0
  54. package/src/lib/server/datasets-route.js +614 -0
  55. package/src/lib/server/datasets-route.js.map +1 -0
  56. package/src/lib/server.d.ts +23 -0
  57. package/src/lib/server.js +54 -0
  58. package/src/lib/server.js.map +1 -0
  59. package/src/lib/site-bundle/datasets-site-bundle.server.js +2 -15
  60. package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -1
  61. package/src/lib/usage-axes.d.ts +24 -0
  62. package/src/lib/usage-axes.js +32 -0
  63. package/src/lib/usage-axes.js.map +1 -0
@@ -0,0 +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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"}
@@ -0,0 +1,26 @@
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 ApiV1ResourceDescription } from '@aglyn/tenant-data-admin/server/api-v1-description';
18
+ /**
19
+ * `/v1/datasets` as the customer API's OpenAPI document describes it — the
20
+ * dataset, its records and the two write bodies — what the console's builder
21
+ * turns into paths, a tag and schemas, and the MCP tools derive from.
22
+ * Transcribed from the customer-facing contract in
23
+ * `apps/docs/api/resources/datasets.md`; `api-v1-openapi.spec.ts` holds the
24
+ * document to every endpoint the documentation promises.
25
+ */
26
+ export declare const DATASETS_API_V1_DESCRIPTION: ApiV1ResourceDescription;
@@ -0,0 +1,260 @@
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
+ */ import { isoField as ISO, nullableField as nullable, objectKindField as OBJECT_FIELD, stringField as str, stringListField as strList } from "@aglyn/tenant-data-admin/server/api-v1-description";
17
+ /**
18
+ * `/v1/datasets` as the customer API's OpenAPI document describes it — the
19
+ * dataset, its records and the two write bodies — what the console's builder
20
+ * turns into paths, a tag and schemas, and the MCP tools derive from.
21
+ * Transcribed from the customer-facing contract in
22
+ * `apps/docs/api/resources/datasets.md`; `api-v1-openapi.spec.ts` holds the
23
+ * document to every endpoint the documentation promises.
24
+ */ export const DATASETS_API_V1_DESCRIPTION = {
25
+ tag: 'Datasets',
26
+ description: 'Structured collections and the records inside them.',
27
+ schemaName: 'Dataset',
28
+ required: [
29
+ 'id',
30
+ 'object',
31
+ 'name',
32
+ 'fields'
33
+ ],
34
+ fields: {
35
+ id: str('Dataset id.'),
36
+ object: OBJECT_FIELD('dataset'),
37
+ name: str('Display name; `""` when none is set.'),
38
+ fields: strList('Field ids in the dataset’s model. The model itself — types, limits, display names — is not returned.'),
39
+ created: nullable(ISO('When the dataset was created.'))
40
+ },
41
+ ops: [
42
+ {
43
+ path: '/v1/datasets',
44
+ method: 'get',
45
+ operationId: 'listDatasets',
46
+ summary: 'List datasets',
47
+ list: true,
48
+ returns: 'Dataset'
49
+ },
50
+ {
51
+ path: '/v1/datasets',
52
+ method: 'post',
53
+ operationId: 'createDataset',
54
+ summary: 'Create a dataset',
55
+ accepts: 'DatasetWrite',
56
+ returns: 'Dataset',
57
+ creates: true
58
+ },
59
+ {
60
+ path: '/v1/datasets/{datasetId}',
61
+ method: 'get',
62
+ operationId: 'getDataset',
63
+ summary: 'Retrieve a dataset',
64
+ returns: 'Dataset',
65
+ pathParams: [
66
+ {
67
+ name: 'datasetId',
68
+ description: 'Dataset id.'
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ path: '/v1/datasets/{datasetId}',
74
+ method: 'patch',
75
+ operationId: 'updateDataset',
76
+ summary: 'Update a dataset',
77
+ accepts: 'DatasetWrite',
78
+ returns: 'Dataset',
79
+ pathParams: [
80
+ {
81
+ name: 'datasetId',
82
+ description: 'Dataset id.'
83
+ }
84
+ ]
85
+ },
86
+ {
87
+ path: '/v1/datasets/{datasetId}',
88
+ method: 'delete',
89
+ operationId: 'deleteDataset',
90
+ summary: 'Delete a dataset',
91
+ returns: 'Deleted',
92
+ description: 'Refuses with `409 conflict` (`code: "dataset_not_empty"`) while the dataset still holds records.',
93
+ pathParams: [
94
+ {
95
+ name: 'datasetId',
96
+ description: 'Dataset id.'
97
+ }
98
+ ]
99
+ },
100
+ {
101
+ path: '/v1/datasets/{datasetId}/records',
102
+ method: 'get',
103
+ operationId: 'listDatasetRecords',
104
+ summary: 'List records',
105
+ list: true,
106
+ returns: 'DatasetRecord',
107
+ pathParams: [
108
+ {
109
+ name: 'datasetId',
110
+ description: 'Dataset id.'
111
+ }
112
+ ]
113
+ },
114
+ {
115
+ path: '/v1/datasets/{datasetId}/records',
116
+ method: 'post',
117
+ operationId: 'createDatasetRecord',
118
+ summary: 'Create a record',
119
+ accepts: 'DatasetRecordWrite',
120
+ returns: 'DatasetRecord',
121
+ creates: true,
122
+ pathParams: [
123
+ {
124
+ name: 'datasetId',
125
+ description: 'Dataset id.'
126
+ }
127
+ ]
128
+ },
129
+ {
130
+ path: '/v1/datasets/{datasetId}/records/{recordId}',
131
+ method: 'get',
132
+ operationId: 'getDatasetRecord',
133
+ summary: 'Retrieve a record',
134
+ returns: 'DatasetRecord',
135
+ pathParams: [
136
+ {
137
+ name: 'datasetId',
138
+ description: 'Dataset id.'
139
+ },
140
+ {
141
+ name: 'recordId',
142
+ description: 'Record id.'
143
+ }
144
+ ]
145
+ },
146
+ {
147
+ path: '/v1/datasets/{datasetId}/records/{recordId}',
148
+ method: 'patch',
149
+ operationId: 'updateDatasetRecord',
150
+ summary: 'Update a record',
151
+ accepts: 'DatasetRecordWrite',
152
+ returns: 'DatasetRecord',
153
+ pathParams: [
154
+ {
155
+ name: 'datasetId',
156
+ description: 'Dataset id.'
157
+ },
158
+ {
159
+ name: 'recordId',
160
+ description: 'Record id.'
161
+ }
162
+ ]
163
+ },
164
+ {
165
+ path: '/v1/datasets/{datasetId}/records/{recordId}',
166
+ method: 'delete',
167
+ operationId: 'deleteDatasetRecord',
168
+ summary: 'Delete a record',
169
+ returns: 'Deleted',
170
+ pathParams: [
171
+ {
172
+ name: 'datasetId',
173
+ description: 'Dataset id.'
174
+ },
175
+ {
176
+ name: 'recordId',
177
+ description: 'Record id.'
178
+ }
179
+ ]
180
+ }
181
+ ],
182
+ components: {
183
+ DatasetRecord: {
184
+ type: 'object',
185
+ description: 'A row in a dataset. Its values sit under `values`, keyed by field id — ' + 'whatever that dataset’s model defines, so they cannot be enumerated here.',
186
+ required: [
187
+ 'id',
188
+ 'object',
189
+ 'values'
190
+ ],
191
+ properties: {
192
+ id: {
193
+ type: 'string',
194
+ description: 'Record id. Opaque — do not pattern-match it.'
195
+ },
196
+ object: {
197
+ type: 'string',
198
+ const: 'record',
199
+ description: 'Always `record`.'
200
+ },
201
+ values: {
202
+ type: 'object',
203
+ description: 'Field id → value. A timestamp field comes back as epoch milliseconds, ' + 'not an ISO string.',
204
+ additionalProperties: true
205
+ },
206
+ created: {
207
+ type: [
208
+ 'string',
209
+ 'null'
210
+ ],
211
+ format: 'date-time'
212
+ },
213
+ updated: {
214
+ type: [
215
+ 'string',
216
+ 'null'
217
+ ],
218
+ format: 'date-time'
219
+ }
220
+ }
221
+ },
222
+ DatasetRecordWrite: {
223
+ type: 'object',
224
+ description: 'A record’s values, under `values` and keyed by field id.\n\n' + 'Only `values` is read. Any other member — `id` included — is ignored ' + 'rather than rejected, and so is a field id inside `values` that the ' + 'dataset’s model does not define. Strings are coerced to the field’s ' + 'type, and a `PATCH` merges `values` shallowly over the stored ones.',
225
+ properties: {
226
+ values: {
227
+ type: 'object',
228
+ description: 'Field id → value, per the dataset’s model.',
229
+ additionalProperties: true
230
+ }
231
+ },
232
+ additionalProperties: true
233
+ },
234
+ DatasetWrite: {
235
+ type: 'object',
236
+ description: 'The writable half of a dataset. A create needs `name` and `fields`; a ' + '`PATCH` takes any of the three and leaves an omitted one as it is.\n\n' + 'Members this body does not name are ignored rather than rejected — ' + '`id` and `created` included.',
237
+ properties: {
238
+ name: {
239
+ type: 'string',
240
+ description: 'Display name. Trimmed, and truncated to 120 characters.'
241
+ },
242
+ fields: {
243
+ type: 'array',
244
+ items: {
245
+ type: 'string'
246
+ },
247
+ description: 'Field ids. Blanks are dropped and at most 100 kept; an empty result ' + 'is a `400`. A `PATCH` replaces the list rather than merging it.'
248
+ },
249
+ model: {
250
+ type: 'object',
251
+ additionalProperties: true,
252
+ description: 'The typed field model. Stored as sent when it serializes to under ' + '64 KB; its shape is not validated, and it is never returned.'
253
+ }
254
+ },
255
+ additionalProperties: true
256
+ }
257
+ }
258
+ };
259
+
260
+ //# sourceMappingURL=openapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/data/src/lib/server/api-v1/openapi.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 type ApiV1ResourceDescription,\n isoField as ISO,\n nullableField as nullable,\n objectKindField as OBJECT_FIELD,\n stringField as str,\n stringListField as strList,\n} from '@aglyn/tenant-data-admin/server/api-v1-description'\n\n/**\n * `/v1/datasets` as the customer API's OpenAPI document describes it — the\n * dataset, its records and the two write bodies — what the console's builder\n * turns into paths, a tag and schemas, and the MCP tools derive from.\n * Transcribed from the customer-facing contract in\n * `apps/docs/api/resources/datasets.md`; `api-v1-openapi.spec.ts` holds the\n * document to every endpoint the documentation promises.\n */\nexport const DATASETS_API_V1_DESCRIPTION: ApiV1ResourceDescription = {\n tag: 'Datasets',\n description: 'Structured collections and the records inside them.',\n schemaName: 'Dataset',\n required: ['id', 'object', 'name', 'fields'],\n fields: {\n id: str('Dataset id.'),\n object: OBJECT_FIELD('dataset'),\n name: str('Display name; `\"\"` when none is set.'),\n fields: strList(\n 'Field ids in the dataset’s model. The model itself — types, limits, display names — is not returned.',\n ),\n created: nullable(ISO('When the dataset was created.')),\n },\n ops: [\n { path: '/v1/datasets', method: 'get', operationId: 'listDatasets', summary: 'List datasets', list: true, returns: 'Dataset' },\n { path: '/v1/datasets', method: 'post', operationId: 'createDataset', summary: 'Create a dataset', accepts: 'DatasetWrite', returns: 'Dataset', creates: true },\n { path: '/v1/datasets/{datasetId}', method: 'get', operationId: 'getDataset', summary: 'Retrieve a dataset', returns: 'Dataset', pathParams: [{ name: 'datasetId', description: 'Dataset id.' }] },\n { path: '/v1/datasets/{datasetId}', method: 'patch', operationId: 'updateDataset', summary: 'Update a dataset', accepts: 'DatasetWrite', returns: 'Dataset', pathParams: [{ name: 'datasetId', description: 'Dataset id.' }] },\n {\n path: '/v1/datasets/{datasetId}', method: 'delete', operationId: 'deleteDataset', summary: 'Delete a dataset', returns: 'Deleted',\n description: 'Refuses with `409 conflict` (`code: \"dataset_not_empty\"`) while the dataset still holds records.',\n pathParams: [{ name: 'datasetId', description: 'Dataset id.' }],\n },\n { path: '/v1/datasets/{datasetId}/records', method: 'get', operationId: 'listDatasetRecords', summary: 'List records', list: true, returns: 'DatasetRecord', pathParams: [{ name: 'datasetId', description: 'Dataset id.' }] },\n { path: '/v1/datasets/{datasetId}/records', method: 'post', operationId: 'createDatasetRecord', summary: 'Create a record', accepts: 'DatasetRecordWrite', returns: 'DatasetRecord', creates: true, pathParams: [{ name: 'datasetId', description: 'Dataset id.' }] },\n { path: '/v1/datasets/{datasetId}/records/{recordId}', method: 'get', operationId: 'getDatasetRecord', summary: 'Retrieve a record', returns: 'DatasetRecord', pathParams: [{ name: 'datasetId', description: 'Dataset id.' }, { name: 'recordId', description: 'Record id.' }] },\n { path: '/v1/datasets/{datasetId}/records/{recordId}', method: 'patch', operationId: 'updateDatasetRecord', summary: 'Update a record', accepts: 'DatasetRecordWrite', returns: 'DatasetRecord', pathParams: [{ name: 'datasetId', description: 'Dataset id.' }, { name: 'recordId', description: 'Record id.' }] },\n { path: '/v1/datasets/{datasetId}/records/{recordId}', method: 'delete', operationId: 'deleteDatasetRecord', summary: 'Delete a record', returns: 'Deleted', pathParams: [{ name: 'datasetId', description: 'Dataset id.' }, { name: 'recordId', description: 'Record id.' }] },\n ],\n components: {\n DatasetRecord: {\n type: 'object',\n description:\n 'A row in a dataset. Its values sit under `values`, keyed by field id — ' +\n 'whatever that dataset’s model defines, so they cannot be enumerated here.',\n required: ['id', 'object', 'values'],\n properties: {\n id: { type: 'string', description: 'Record id. Opaque — do not pattern-match it.' },\n object: { type: 'string', const: 'record', description: 'Always `record`.' },\n values: {\n type: 'object',\n description:\n 'Field id → value. A timestamp field comes back as epoch milliseconds, ' +\n 'not an ISO string.',\n additionalProperties: true,\n },\n created: { type: ['string', 'null'], format: 'date-time' },\n updated: { type: ['string', 'null'], format: 'date-time' },\n },\n },\n DatasetRecordWrite: {\n type: 'object',\n description:\n 'A record’s values, under `values` and keyed by field id.\\n\\n' +\n 'Only `values` is read. Any other member — `id` included — is ignored ' +\n 'rather than rejected, and so is a field id inside `values` that the ' +\n 'dataset’s model does not define. Strings are coerced to the field’s ' +\n 'type, and a `PATCH` merges `values` shallowly over the stored ones.',\n properties: {\n values: {\n type: 'object',\n description: 'Field id → value, per the dataset’s model.',\n additionalProperties: true,\n },\n },\n additionalProperties: true,\n },\n DatasetWrite: {\n type: 'object',\n description:\n 'The writable half of a dataset. A create needs `name` and `fields`; a ' +\n '`PATCH` takes any of the three and leaves an omitted one as it is.\\n\\n' +\n 'Members this body does not name are ignored rather than rejected — ' +\n '`id` and `created` included.',\n properties: {\n name: { type: 'string', description: 'Display name. Trimmed, and truncated to 120 characters.' },\n fields: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Field ids. Blanks are dropped and at most 100 kept; an empty result ' +\n 'is a `400`. A `PATCH` replaces the list rather than merging it.',\n },\n model: {\n type: 'object',\n additionalProperties: true,\n description:\n 'The typed field model. Stored as sent when it serializes to under ' +\n '64 KB; its shape is not validated, and it is never returned.',\n },\n },\n additionalProperties: true,\n },\n },\n}\n"],"names":["isoField","ISO","nullableField","nullable","objectKindField","OBJECT_FIELD","stringField","str","stringListField","strList","DATASETS_API_V1_DESCRIPTION","tag","description","schemaName","required","fields","id","object","name","created","ops","path","method","operationId","summary","list","returns","accepts","creates","pathParams","components","DatasetRecord","type","properties","const","values","additionalProperties","format","updated","DatasetRecordWrite","DatasetWrite","items","model"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAEEA,YAAYC,GAAG,EACfC,iBAAiBC,QAAQ,EACzBC,mBAAmBC,YAAY,EAC/BC,eAAeC,GAAG,EAClBC,mBAAmBC,OAAO,QACrB,qDAAoD;AAE3D;;;;;;;CAOC,GACD,OAAO,MAAMC,8BAAwD;IACnEC,KAAK;IACLC,aAAa;IACbC,YAAY;IACZC,UAAU;QAAC;QAAM;QAAU;QAAQ;KAAS;IAC5CC,QAAQ;QACNC,IAAIT,IAAI;QACRU,QAAQZ,aAAa;QACrBa,MAAMX,IAAI;QACVQ,QAAQN,QACN;QAEFU,SAAShB,SAASF,IAAI;IACxB;IACAmB,KAAK;QACH;YAAEC,MAAM;YAAgBC,QAAQ;YAAOC,aAAa;YAAgBC,SAAS;YAAiBC,MAAM;YAAMC,SAAS;QAAU;QAC7H;YAAEL,MAAM;YAAgBC,QAAQ;YAAQC,aAAa;YAAiBC,SAAS;YAAoBG,SAAS;YAAgBD,SAAS;YAAWE,SAAS;QAAK;QAC9J;YAAEP,MAAM;YAA4BC,QAAQ;YAAOC,aAAa;YAAcC,SAAS;YAAsBE,SAAS;YAAWG,YAAY;gBAAC;oBAAEX,MAAM;oBAAaN,aAAa;gBAAc;aAAE;QAAC;QACjM;YAAES,MAAM;YAA4BC,QAAQ;YAASC,aAAa;YAAiBC,SAAS;YAAoBG,SAAS;YAAgBD,SAAS;YAAWG,YAAY;gBAAC;oBAAEX,MAAM;oBAAaN,aAAa;gBAAc;aAAE;QAAC;QAC7N;YACES,MAAM;YAA4BC,QAAQ;YAAUC,aAAa;YAAiBC,SAAS;YAAoBE,SAAS;YACxHd,aAAa;YACbiB,YAAY;gBAAC;oBAAEX,MAAM;oBAAaN,aAAa;gBAAc;aAAE;QACjE;QACA;YAAES,MAAM;YAAoCC,QAAQ;YAAOC,aAAa;YAAsBC,SAAS;YAAgBC,MAAM;YAAMC,SAAS;YAAiBG,YAAY;gBAAC;oBAAEX,MAAM;oBAAaN,aAAa;gBAAc;aAAE;QAAC;QAC7N;YAAES,MAAM;YAAoCC,QAAQ;YAAQC,aAAa;YAAuBC,SAAS;YAAmBG,SAAS;YAAsBD,SAAS;YAAiBE,SAAS;YAAMC,YAAY;gBAAC;oBAAEX,MAAM;oBAAaN,aAAa;gBAAc;aAAE;QAAC;QACpQ;YAAES,MAAM;YAA+CC,QAAQ;YAAOC,aAAa;YAAoBC,SAAS;YAAqBE,SAAS;YAAiBG,YAAY;gBAAC;oBAAEX,MAAM;oBAAaN,aAAa;gBAAc;gBAAG;oBAAEM,MAAM;oBAAYN,aAAa;gBAAa;aAAE;QAAC;QAChR;YAAES,MAAM;YAA+CC,QAAQ;YAASC,aAAa;YAAuBC,SAAS;YAAmBG,SAAS;YAAsBD,SAAS;YAAiBG,YAAY;gBAAC;oBAAEX,MAAM;oBAAaN,aAAa;gBAAc;gBAAG;oBAAEM,MAAM;oBAAYN,aAAa;gBAAa;aAAE;QAAC;QAClT;YAAES,MAAM;YAA+CC,QAAQ;YAAUC,aAAa;YAAuBC,SAAS;YAAmBE,SAAS;YAAWG,YAAY;gBAAC;oBAAEX,MAAM;oBAAaN,aAAa;gBAAc;gBAAG;oBAAEM,MAAM;oBAAYN,aAAa;gBAAa;aAAE;QAAC;KAC/Q;IACDkB,YAAY;QACVC,eAAe;YACbC,MAAM;YACNpB,aACE,4EACA;YACFE,UAAU;gBAAC;gBAAM;gBAAU;aAAS;YACpCmB,YAAY;gBACVjB,IAAI;oBAAEgB,MAAM;oBAAUpB,aAAa;gBAA+C;gBAClFK,QAAQ;oBAAEe,MAAM;oBAAUE,OAAO;oBAAUtB,aAAa;gBAAmB;gBAC3EuB,QAAQ;oBACNH,MAAM;oBACNpB,aACE,2EACA;oBACFwB,sBAAsB;gBACxB;gBACAjB,SAAS;oBAAEa,MAAM;wBAAC;wBAAU;qBAAO;oBAAEK,QAAQ;gBAAY;gBACzDC,SAAS;oBAAEN,MAAM;wBAAC;wBAAU;qBAAO;oBAAEK,QAAQ;gBAAY;YAC3D;QACF;QACAE,oBAAoB;YAClBP,MAAM;YACNpB,aACE,iEACA,0EACA,yEACA,yEACA;YACFqB,YAAY;gBACVE,QAAQ;oBACNH,MAAM;oBACNpB,aAAa;oBACbwB,sBAAsB;gBACxB;YACF;YACAA,sBAAsB;QACxB;QACAI,cAAc;YACZR,MAAM;YACNpB,aACE,2EACA,2EACA,wEACA;YACFqB,YAAY;gBACVf,MAAM;oBAAEc,MAAM;oBAAUpB,aAAa;gBAA0D;gBAC/FG,QAAQ;oBACNiB,MAAM;oBACNS,OAAO;wBAAET,MAAM;oBAAS;oBACxBpB,aACE,yEACA;gBACJ;gBACA8B,OAAO;oBACLV,MAAM;oBACNI,sBAAsB;oBACtBxB,aACE,uEACA;gBACJ;YACF;YACAwB,sBAAsB;QACxB;IACF;AACF,EAAC"}
@@ -0,0 +1,26 @@
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 ApiV1Context } from '@aglyn/tenant-data-admin/server/api-v1-kit';
18
+ /**
19
+ * The organization's datasets band on `GET /v1/usage` (AGL-2277, AGL-3080).
20
+ *
21
+ * Datasets are the one band with no overage RATE — extra slots are an add-on
22
+ * the organization buys, not usage that meters — so `metered` is always false
23
+ * and `included` is the effective limit INCLUDING purchased add-ons, which is
24
+ * the number that actually refuses a create.
25
+ */
26
+ export declare function datasetUsageFigures(ctx: ApiV1Context): Promise<Record<string, unknown>>;
@@ -0,0 +1,33 @@
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
+ */ import { checkDatasetQuota } from "@aglyn/aglyn/app-utils/plan-entitlements";
17
+ import { usageBand } from "@aglyn/tenant-data-admin/server/api-v1-kit";
18
+ /**
19
+ * The organization's datasets band on `GET /v1/usage` (AGL-2277, AGL-3080).
20
+ *
21
+ * Datasets are the one band with no overage RATE — extra slots are an add-on
22
+ * the organization buys, not usage that meters — so `metered` is always false
23
+ * and `included` is the effective limit INCLUDING purchased add-ons, which is
24
+ * the number that actually refuses a create.
25
+ */ export async function datasetUsageFigures(ctx) {
26
+ const held = (await ctx.firestore.collection('orgs').doc(ctx.orgId).collection('datasets').count().get()).data().count;
27
+ const quota = checkDatasetQuota(ctx.org, held);
28
+ return {
29
+ datasets: usageBand(held, quota.limit, quota.remaining, null)
30
+ };
31
+ }
32
+
33
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/data/src/lib/server/api-v1/usage.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 { checkDatasetQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n type ApiV1Context,\n usageBand,\n} from '@aglyn/tenant-data-admin/server/api-v1-kit'\n\n/**\n * The organization's datasets band on `GET /v1/usage` (AGL-2277, AGL-3080).\n *\n * Datasets are the one band with no overage RATE — extra slots are an add-on\n * the organization buys, not usage that meters — so `metered` is always false\n * and `included` is the effective limit INCLUDING purchased add-ons, which is\n * the number that actually refuses a create.\n */\nexport async function datasetUsageFigures(\n ctx: ApiV1Context,\n): Promise<Record<string, unknown>> {\n const held = (\n await ctx.firestore\n .collection('orgs')\n .doc(ctx.orgId)\n .collection('datasets')\n .count()\n .get()\n ).data().count\n const quota = checkDatasetQuota(ctx.org, held)\n return { datasets: usageBand(held, quota.limit, quota.remaining, null) }\n}\n"],"names":["checkDatasetQuota","usageBand","datasetUsageFigures","ctx","held","firestore","collection","doc","orgId","count","get","data","quota","org","datasets","limit","remaining"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,2CAA0C;AAC5E,SAEEC,SAAS,QACJ,6CAA4C;AAEnD;;;;;;;CAOC,GACD,OAAO,eAAeC,oBACpBC,GAAiB;IAEjB,MAAMC,OAAO,AACX,CAAA,MAAMD,IAAIE,SAAS,CAChBC,UAAU,CAAC,QACXC,GAAG,CAACJ,IAAIK,KAAK,EACbF,UAAU,CAAC,YACXG,KAAK,GACLC,GAAG,EAAC,EACPC,IAAI,GAAGF,KAAK;IACd,MAAMG,QAAQZ,kBAAkBG,IAAIU,GAAG,EAAET;IACzC,OAAO;QAAEU,UAAUb,UAAUG,MAAMQ,MAAMG,KAAK,EAAEH,MAAMI,SAAS,EAAE;IAAM;AACzE"}
@@ -0,0 +1,39 @@
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
+ /** A model's fields, as far as this check reads them. */
18
+ interface FieldModel {
19
+ fields?: Record<string, {
20
+ customType?: string;
21
+ } | undefined>;
22
+ order?: readonly string[];
23
+ }
24
+ /**
25
+ * Makes sure every custom field type a dataset model names is registered
26
+ * before a record is validated against it (AGL-434).
27
+ *
28
+ * `validateDocument` answers "no error" for a type nobody registered — right
29
+ * for a plugin that is genuinely absent, wrong for one that has not loaded in
30
+ * this process yet. A type is registered when its plugin's console server
31
+ * surface loads, or from its server declarations, so a model that names one
32
+ * asks the caller to load the surfaces (`loadPluginSurfaces`, handed in by
33
+ * the `/v1` router and by a site restore) and then runs the platform's own
34
+ * declarations repair. A model with no custom field pays for neither. A load
35
+ * that fails propagates: storing a value its validator never saw is the
36
+ * defect this exists to prevent.
37
+ */
38
+ export declare function loadCustomFieldTypes(model: FieldModel | null | undefined, loadPluginSurfaces?: () => Promise<void>): Promise<void>;
39
+ export {};
@@ -0,0 +1,41 @@
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
+ */ import { ensureDeclaredCustomFieldTypes } from "@aglyn/aglyn/server";
17
+ /**
18
+ * Makes sure every custom field type a dataset model names is registered
19
+ * before a record is validated against it (AGL-434).
20
+ *
21
+ * `validateDocument` answers "no error" for a type nobody registered — right
22
+ * for a plugin that is genuinely absent, wrong for one that has not loaded in
23
+ * this process yet. A type is registered when its plugin's console server
24
+ * surface loads, or from its server declarations, so a model that names one
25
+ * asks the caller to load the surfaces (`loadPluginSurfaces`, handed in by
26
+ * the `/v1` router and by a site restore) and then runs the platform's own
27
+ * declarations repair. A model with no custom field pays for neither. A load
28
+ * that fails propagates: storing a value its validator never saw is the
29
+ * defect this exists to prevent.
30
+ */ export async function loadCustomFieldTypes(model, loadPluginSurfaces) {
31
+ var _ref;
32
+ const usesCustomType = ((_ref = model == null ? void 0 : model.order) != null ? _ref : []).some((fieldId)=>{
33
+ var _model_fields_fieldId, _model_fields;
34
+ return Boolean(model == null ? void 0 : (_model_fields = model.fields) == null ? void 0 : (_model_fields_fieldId = _model_fields[fieldId]) == null ? void 0 : _model_fields_fieldId.customType);
35
+ });
36
+ if (!usesCustomType) return;
37
+ if (loadPluginSurfaces) await loadPluginSurfaces();
38
+ await ensureDeclaredCustomFieldTypes(model);
39
+ }
40
+
41
+ //# sourceMappingURL=custom-field-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/custom-field-types.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 { ensureDeclaredCustomFieldTypes } from '@aglyn/aglyn/server'\n\n/** A model's fields, as far as this check reads them. */\ninterface FieldModel {\n fields?: Record<string, { customType?: string } | undefined>\n order?: readonly string[]\n}\n\n/**\n * Makes sure every custom field type a dataset model names is registered\n * before a record is validated against it (AGL-434).\n *\n * `validateDocument` answers \"no error\" for a type nobody registered — right\n * for a plugin that is genuinely absent, wrong for one that has not loaded in\n * this process yet. A type is registered when its plugin's console server\n * surface loads, or from its server declarations, so a model that names one\n * asks the caller to load the surfaces (`loadPluginSurfaces`, handed in by\n * the `/v1` router and by a site restore) and then runs the platform's own\n * declarations repair. A model with no custom field pays for neither. A load\n * that fails propagates: storing a value its validator never saw is the\n * defect this exists to prevent.\n */\nexport async function loadCustomFieldTypes(\n model: FieldModel | null | undefined,\n loadPluginSurfaces?: () => Promise<void>,\n): Promise<void> {\n const usesCustomType = (model?.order ?? []).some((fieldId) =>\n Boolean(model?.fields?.[fieldId]?.customType),\n )\n if (!usesCustomType) return\n if (loadPluginSurfaces) await loadPluginSurfaces()\n await ensureDeclaredCustomFieldTypes(model)\n}\n"],"names":["ensureDeclaredCustomFieldTypes","loadCustomFieldTypes","model","loadPluginSurfaces","usesCustomType","order","some","fieldId","Boolean","fields","customType"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,8BAA8B,QAAQ,sBAAqB;AAQpE;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAeC,qBACpBC,KAAoC,EACpCC,kBAAwC;;IAExC,MAAMC,iBAAiB,SAACF,yBAAAA,MAAOG,KAAK,mBAAI,EAAE,EAAEC,IAAI,CAAC,CAACC;YACxCL,uBAAAA;eAARM,QAAQN,0BAAAA,gBAAAA,MAAOO,MAAM,sBAAbP,wBAAAA,aAAe,CAACK,QAAQ,qBAAxBL,sBAA0BQ,UAAU;;IAE9C,IAAI,CAACN,gBAAgB;IACrB,IAAID,oBAAoB,MAAMA;IAC9B,MAAMH,+BAA+BE;AACvC"}