@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
package/README.md CHANGED
@@ -28,11 +28,15 @@ The plugin is console-only and declares one registrar, `registerDataConsole` (th
28
28
  - The organization datasets card, `HostDatasetsCard`, as a widget in the `orgData` slot.
29
29
  - Datasets as a repeat source (`DATASET_REPEAT_SOURCE`), which is what offers "Repeat over dataset" on an element in Besigner and draws a repeat's copies from real rows on the canvas.
30
30
 
31
+ It also keeps the `datasetSchema` installable artifact type (`artifactTypes` in `plugins.config.json`, registered from `registerDataConsoleServerDeclarations`): a dataset's schema published to a marketplace, installed as a new, empty dataset, and updated in place. The installer asks this plugin through `@aglyn/aglyn/plugin-manager/plugin-artifact-types` and never reads the datasets collection; the schema's model (`sanitizeDatasetSchema`, `resolveInstalledDatasetSchema`, `summarizeSchemaChange`) is `@aglyn/plugins-data/artifact/dataset-schema`.
32
+
33
+ On the server, `registerDataServerDeclarations()` also registers the two automation steps that write a dataset record, `datasetAppend` and `updateDataset` (`server/dataset-steps.server.ts`). The Workflows engine runs them through the platform's server-step seam (`registerServerStepExecutor`), and `plugins.config.json` declares them under `serverSteps`. A record a step writes refreshes the live pages that repeat over its dataset (`server/dataset-live-pages.ts`).
34
+
31
35
  ### Entry points
32
36
 
33
37
  | import | contents |
34
38
  | -- | -- |
35
- | `@aglyn/plugins-data` | `BUNDLE_ID`, `registerDataConsole`, `HostDatasetsCard`, and the import/export model: `parseImportRows`, `mapImportColumns`, the dataset CSV/JSON writers (`datasetRecordsToCsv`, `datasetCsvHeader`, `datasetCsvRow`, `datasetRecordToJson`, `serializeDatasetValue`), and the platform's CSV helpers re-exported from the core (`parseCsv`, `countCsvDataRows`, `exportShortfall`) |
39
+ | `@aglyn/plugins-data` | `BUNDLE_ID`, `registerDataConsole`, `HostDatasetsCard`, and the transfer resource a dataset's records move as: `DATASET_TRANSFER_RESOURCE` (`data.dataset`) and `datasetTransferResourceKey(datasetId)` |
36
40
  | `@aglyn/plugins-data/*` | any module under `src/lib/`, for example `@aglyn/plugins-data/model/dataset-record-view` (`datasetRecordFields`, `describeDatasetValue`) |
37
41
 
38
42
  ## Usage
@@ -44,14 +48,13 @@ import { registerDataConsole } from '@aglyn/plugins-data'
44
48
  registerDataConsole()
45
49
  ```
46
50
 
47
- The import helpers are pure functions:
51
+ Import and export run on the platform's transfer framework (AGL-3530). The plugin declares `data.dataset` under `transferResources` with `instances`, so each dataset is its own resource, `data.dataset:<datasetId>`; it registers the server half from `registerDataConsoleServerDeclarations` (`transfer/dataset-transfer.server.ts`: the field catalog, match keys, the export's pages and count, the dry run held to the dataset's model, the writes and their undo) and the client half from `registerDataConsole`. The Data card opens the wizard and the export dialog through the console's launcher:
48
52
 
49
53
  ```ts
50
- import { parseImportRows } from '@aglyn/plugins-data'
54
+ import { useTransferLauncher } from '@aglyn/aglyn'
55
+ import { datasetTransferResourceKey } from '@aglyn/plugins-data'
51
56
 
52
- // A JSON array of objects, or CSV with a header row.
53
- // Returns string maps keyed by source column name, or null.
54
- const rows = parseImportRows('name,city\nAda,London')
57
+ useTransferLauncher()?.openImport({ resource: datasetTransferResourceKey(datasetId), scope: 'org' })
55
58
  ```
56
59
 
57
60
  ## How it fits
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-data",
3
- "version": "1.0.0-beta.220",
3
+ "version": "1.0.0-beta.222",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,15 +25,15 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.220",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.220",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.220",
31
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.220",
32
- "@aglyn/shared-util-http": "1.0.0-beta.220",
33
- "@aglyn/shared-util-timestamp": "1.0.0-beta.220",
34
- "@aglyn/tenant-data-admin": "1.0.0-beta.220",
35
- "@aglyn/tenant-feature-instance": "1.0.0-beta.220",
36
- "@aglyn/tenant-runtime": "1.0.0-beta.220",
28
+ "@aglyn/aglyn": "1.0.0-beta.222",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.222",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.222",
31
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.222",
32
+ "@aglyn/shared-util-http": "1.0.0-beta.222",
33
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.222",
34
+ "@aglyn/tenant-data-admin": "1.0.0-beta.222",
35
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.222",
36
+ "@aglyn/tenant-runtime": "1.0.0-beta.222",
37
37
  "@swc/helpers": "0.5.23"
38
38
  },
39
39
  "peerDependencies": {
package/src/index.d.ts CHANGED
@@ -16,4 +16,4 @@
16
16
  */
17
17
  export * from './lib/constants/bundle-common';
18
18
  export * from './lib/plugin';
19
- export * from './lib/model';
19
+ export * from './lib/transfer/dataset-transfer-key';
package/src/index.js CHANGED
@@ -15,6 +15,6 @@
15
15
  * limitations under the License.
16
16
  */ export * from "./lib/constants/bundle-common.js";
17
17
  export * from "./lib/plugin.js";
18
- export * from "./lib/model/index.js";
18
+ export * from "./lib/transfer/dataset-transfer-key.js";
19
19
 
20
20
  //# sourceMappingURL=index.js.map
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../libs/plugins/data/src/index.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\nexport * from './lib/constants/bundle-common'\nexport * from './lib/plugin'\nexport * from './lib/model'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,mCAA+B;AAC7C,cAAc,kBAAc;AAC5B,cAAc,uBAAa"}
1
+ {"version":3,"sources":["../../../../../libs/plugins/data/src/index.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\nexport * from './lib/constants/bundle-common'\nexport * from './lib/plugin'\nexport * from './lib/transfer/dataset-transfer-key'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,mCAA+B;AAC7C,cAAc,kBAAc;AAC5B,cAAc,yCAAqC"}
@@ -0,0 +1,57 @@
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 { ArtifactInstallRequest, ArtifactLocateRequest, ArtifactRefusal, ArtifactSnapshot, ArtifactSnapshotRequest, ArtifactWorkspace, InstalledArtifactCopy, PluginArtifactOwner, PreparedArtifactInstall } from '@aglyn/aglyn/plugin-manager/plugin-artifact-types';
18
+ /**
19
+ * PUBLISHING: an org dataset's SCHEMA, reduced to what travels.
20
+ *
21
+ * Datasets live at `orgs/{orgId}/datasets/{id}` and are org-shared (AGL-237),
22
+ * so the source is named by the publishing org and the dataset's id.
23
+ *
24
+ * Records NEVER travel: `sanitizeDatasetSchema` reads the model, and nothing
25
+ * here touches the `records` subcollection. A dataset's rows are the org's
26
+ * customer data; only the shape is publishable.
27
+ */
28
+ export declare function snapshotDatasetSchema(request: ArtifactSnapshotRequest): Promise<ArtifactSnapshot | ArtifactRefusal>;
29
+ /** INSTALLING, before the listing is read: datasets are a plan feature. */
30
+ export declare function admitsDatasetSchema(workspace: ArtifactWorkspace): Promise<ArtifactRefusal | null>;
31
+ /**
32
+ * INSTALLING: a NEW, EMPTY dataset made from the published model.
33
+ *
34
+ * Installing never merges into an existing dataset, because a schema change
35
+ * on a dataset that already holds rows would silently reinterpret live data.
36
+ * Re-installing makes another dataset rather than replacing one, for the same
37
+ * reason.
38
+ *
39
+ * Nothing is written until `commit`, which the installer calls once it has
40
+ * recorded the provenance of exactly the content prepared here.
41
+ */
42
+ export declare function prepareDatasetSchemaInstall(request: ArtifactInstallRequest): Promise<PreparedArtifactInstall | ArtifactRefusal>;
43
+ /**
44
+ * UPDATING: the dataset a listing installed in the site's organization, and
45
+ * what taking the version on offer would do to its records (AGL-1018).
46
+ *
47
+ * The incoming schema is relinked onto THIS org's datasets exactly as install
48
+ * does. The base holds the relinked shape, so diffing the published one would
49
+ * report every reference field as a user edit.
50
+ *
51
+ * A dataset's update is a merge or nothing. Its "new copy" would be a whole
52
+ * new dataset, which consumes org quota and lands empty: exactly what the
53
+ * install already does, with the quota check an update does not have.
54
+ */
55
+ export declare function locateInstalledDatasetSchema(request: ArtifactLocateRequest): Promise<InstalledArtifactCopy | ArtifactRefusal>;
56
+ /** The four answers, as the owner registers them. */
57
+ export declare const datasetSchemaArtifactOwner: PluginArtifactOwner;
@@ -0,0 +1,308 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { checkDatasetQuota, checkEntitlement, createResourceUid, defaultScopeForNewResource, newResourceScopeFields, scopeCovers } from "@aglyn/aglyn/server";
18
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
19
+ import { resolveInstalledDatasetSchema, sanitizeDatasetSchema, summarizeSchemaChange } from "./dataset-schema.js";
20
+ /**
21
+ * A dataset's schema as an installable artifact (AGL-657), answered by the
22
+ * plugin that keeps datasets (AGL-3080).
23
+ *
24
+ * The installer (the marketplace) owns the listing, who may act on it, the
25
+ * purchase, the provenance stamp and the tally, and asks these four answers
26
+ * for everything that touches `orgs/{orgId}/datasets`, which it never reads.
27
+ * Registered from `declarations.console-server.ts` and loaded with the first
28
+ * publish, install or update.
29
+ */ const refuse = (status, error)=>({
30
+ ok: false,
31
+ status,
32
+ error
33
+ });
34
+ const datasetsOf = (orgId)=>firebaseAdmin.app().firestore().collection('orgs').doc(orgId).collection('datasets');
35
+ const overDatasetQuota = (limit)=>refuse(403, `Dataset limit reached (${limit}) — see Billing to upgrade.`);
36
+ /**
37
+ * PUBLISHING: an org dataset's SCHEMA, reduced to what travels.
38
+ *
39
+ * Datasets live at `orgs/{orgId}/datasets/{id}` and are org-shared (AGL-237),
40
+ * so the source is named by the publishing org and the dataset's id.
41
+ *
42
+ * Records NEVER travel: `sanitizeDatasetSchema` reads the model, and nothing
43
+ * here touches the `records` subcollection. A dataset's rows are the org's
44
+ * customer data; only the shape is publishable.
45
+ */ export async function snapshotDatasetSchema(request) {
46
+ var _dataset_model;
47
+ const datasets = datasetsOf(request.orgId);
48
+ const datasetSnapshot = await datasets.doc(request.sourceId).get();
49
+ const dataset = datasetSnapshot.data();
50
+ if (!datasetSnapshot.exists || (dataset == null ? void 0 : dataset.deletedAt)) {
51
+ return refuse(404, 'Unknown dataset');
52
+ }
53
+ // v1 datasets carry a flat `fields: string[]` with no model; publish the
54
+ // derived shape so unmigrated datasets are publishable too (AGL-102 shim,
55
+ // inlined rather than importing core's deriveModelFromFields to keep the
56
+ // sanitizer's input one plain shape).
57
+ const model = (dataset == null ? void 0 : (_dataset_model = dataset.model) == null ? void 0 : _dataset_model.fields) ? dataset.model : {
58
+ fields: Object.fromEntries((Array.isArray(dataset == null ? void 0 : dataset.fields) ? dataset.fields : []).map((name)=>[
59
+ String(name),
60
+ {
61
+ name: String(name),
62
+ type: 'text'
63
+ }
64
+ ])),
65
+ order: (Array.isArray(dataset == null ? void 0 : dataset.fields) ? dataset.fields : []).map(String)
66
+ };
67
+ const sanitized = sanitizeDatasetSchema(model);
68
+ if (sanitized.ok === false) return refuse(422, sanitized.error);
69
+ // Reference fields name a dataset id that means nothing outside this org,
70
+ // so resolve each target's display name now: the installer relinks by that
71
+ // label (see resolveInstalledDatasetSchema).
72
+ const referenced = new Set(Object.values(sanitized.schema.fields).map((field)=>{
73
+ var _field_reference;
74
+ return (_field_reference = field.reference) == null ? void 0 : _field_reference.datasetId;
75
+ }).filter((id)=>Boolean(id)));
76
+ if (referenced.size) {
77
+ const labels = new Map();
78
+ await Promise.all([
79
+ ...referenced
80
+ ].map(async (id)=>{
81
+ const snapshot = await datasets.doc(id).get();
82
+ const label = snapshot.get('displayName');
83
+ if (label) labels.set(id, String(label));
84
+ }));
85
+ for (const field of Object.values(sanitized.schema.fields)){
86
+ var _field_reference;
87
+ const target = (_field_reference = field.reference) == null ? void 0 : _field_reference.datasetId;
88
+ if (target && labels.has(target)) {
89
+ field.reference = _extends({}, field.reference, {
90
+ datasetLabel: labels.get(target)
91
+ });
92
+ }
93
+ }
94
+ }
95
+ return {
96
+ ok: true,
97
+ content: sanitized.schema,
98
+ facts: {
99
+ fieldCount: sanitized.schema.order.length
100
+ }
101
+ };
102
+ }
103
+ /** INSTALLING, before the listing is read: datasets are a plan feature. */ export async function admitsDatasetSchema(workspace) {
104
+ if (!checkEntitlement(workspace.org, 'dataStore')) {
105
+ return refuse(403, 'Datasets require a Starter plan or higher');
106
+ }
107
+ return null;
108
+ }
109
+ /**
110
+ * INSTALLING: a NEW, EMPTY dataset made from the published model.
111
+ *
112
+ * Installing never merges into an existing dataset, because a schema change
113
+ * on a dataset that already holds rows would silently reinterpret live data.
114
+ * Re-installing makes another dataset rather than replacing one, for the same
115
+ * reason.
116
+ *
117
+ * Nothing is written until `commit`, which the installer calls once it has
118
+ * recorded the provenance of exactly the content prepared here.
119
+ */ export async function prepareDatasetSchemaInstall(request) {
120
+ var _published_order;
121
+ const published = request.published;
122
+ if (!(published == null ? void 0 : (_published_order = published.order) == null ? void 0 : _published_order.length)) {
123
+ return refuse(500, 'Dataset schema version missing');
124
+ }
125
+ const org = request.org;
126
+ const datasetsRef = datasetsOf(request.orgId);
127
+ // Creating a dataset consumes org quota exactly like the console's own
128
+ // create path (AGL-473): installing must not be a way around it.
129
+ // Deliberately counts EVERY dataset the org owns, not the visible ones
130
+ // (AGL-1046). Scoping decides who can see a dataset, never who pays for
131
+ // it: the org owns all of them. Counting per scope would also make a
132
+ // collaborator's remaining quota disagree with the admin's.
133
+ //
134
+ // This is the early answer, so a workspace already at its cap is refused
135
+ // before the installer records any provenance. The one that holds is in
136
+ // `commit`, below.
137
+ const datasets = await datasetsRef.get();
138
+ const quota = checkDatasetQuota(org, datasets.size);
139
+ if (!quota.allowed) return overDatasetQuota(quota.limit);
140
+ /**
141
+ * Who the new dataset is shared with. Decided before relinking, because
142
+ * it also decides what a reference field may point at.
143
+ *
144
+ * A dataset schema installs at ORGANIZATION scope and nowhere else
145
+ * (`INSTALL_TARGETS.datasetSchema`), from the organization Marketplace, the
146
+ * one surface that installs anything. The site that surface acts THROUGH
147
+ * only finds the org: the org's first site, not a site anyone is working
148
+ * in, and it never reaches here. So no site is in context, and the dataset
149
+ * starts where a create on an organization page starts: All sites, whatever
150
+ * the org's Default sharing says. Honoring the acting site instead would
151
+ * hide the dataset from every other site, under an install dialog that
152
+ * says it lands on "the whole organization — every site" (AGL-2891).
153
+ */ const visibleTo = defaultScopeForNewResource({
154
+ defaultResourceScope: org == null ? void 0 : org.defaultResourceScope,
155
+ hostId: null
156
+ });
157
+ // Relink reference fields onto this org's datasets by display name; what
158
+ // can't be relinked degrades to text and is reported to the installer.
159
+ // A candidate has to be visible everywhere the new dataset is, which is
160
+ // the rule every reference answers to (`scopeCovers`, AGL-1044): one
161
+ // pointing at a dataset some of those sites cannot see resolves to nothing
162
+ // there, and nothing says why. That is also what keeps an install from
163
+ // binding its reference fields to an agency's internal dataset of the same
164
+ // name (AGL-1046): the display-name collision AGL-1039 fixed on the render
165
+ // path, arriving here instead.
166
+ const byLabel = {};
167
+ for (const entry of datasets.docs){
168
+ var _entry_get;
169
+ if (!scopeCovers(entry.get('visibleTo'), visibleTo)) continue;
170
+ const label = String((_entry_get = entry.get('displayName')) != null ? _entry_get : '').toLowerCase();
171
+ if (label && !byLabel[label]) byLabel[label] = entry.id;
172
+ }
173
+ const { schema, degradedFieldIds } = resolveInstalledDatasetSchema(published, byLabel);
174
+ const datasetId = createResourceUid();
175
+ const { listing } = request;
176
+ return {
177
+ ok: true,
178
+ // The RELINKED schema, not the published one, is what the base snapshot
179
+ // records (AGL-1015): relinking rewrites reference fields onto this org's
180
+ // datasets, so a base holding the publisher's ids would report every
181
+ // relinked field as a user edit the moment anything is diffed.
182
+ content: schema,
183
+ /**
184
+ * THE ENFORCEMENT POINT (AGL-3454, the AGL-2371 treatment): the count,
185
+ * the decision and the create in ONE transaction.
186
+ *
187
+ * The count above is taken before the installer awaits the provenance
188
+ * write, and every await is a yield: N installs in flight each read the
189
+ * same pre-count, each find room, and each land, and nothing re-counts
190
+ * afterwards. `tx.get` on the aggregate takes a pessimistic lock on the
191
+ * documents the query matched, so the loser of a race retries, re-reads
192
+ * the higher count and is refused, having written nothing.
193
+ *
194
+ * The org document is read outside, as the console's create path reads
195
+ * it: the plan and the purchased add-ons are not client-writable, so
196
+ * locking it would buy contention rather than correctness.
197
+ */ commit: async (stamp)=>{
198
+ const refused = await firebaseAdmin.app().firestore().runTransaction(async (tx)=>{
199
+ var _listing_displayName;
200
+ const live = (await tx.get(datasetsRef.count())).data().count;
201
+ const authoritative = checkDatasetQuota(org, live);
202
+ if (!authoritative.allowed) return overDatasetQuota(authoritative.limit);
203
+ tx.create(datasetsRef.doc(datasetId), _extends({
204
+ displayName: String((_listing_displayName = listing.displayName) != null ? _listing_displayName : 'Dataset').slice(0, 120)
205
+ }, listing.description ? {
206
+ description: listing.description
207
+ } : {}, {
208
+ // `fields` is the v1 flat list the older editor still reads; keep it
209
+ // in step with the model so both readers agree (AGL-102).
210
+ fields: schema.order,
211
+ model: schema,
212
+ source: stamp.source,
213
+ installedFrom: stamp.installedFrom
214
+ }, newResourceScopeFields(visibleTo), {
215
+ createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
216
+ }));
217
+ return null;
218
+ });
219
+ if (refused) return refused;
220
+ return {
221
+ ok: true,
222
+ report: {
223
+ datasetId,
224
+ fields: schema.order.length,
225
+ degradedFieldIds
226
+ }
227
+ };
228
+ }
229
+ };
230
+ }
231
+ /**
232
+ * UPDATING: the dataset a listing installed in the site's organization, and
233
+ * what taking the version on offer would do to its records (AGL-1018).
234
+ *
235
+ * The incoming schema is relinked onto THIS org's datasets exactly as install
236
+ * does. The base holds the relinked shape, so diffing the published one would
237
+ * report every reference field as a user edit.
238
+ *
239
+ * A dataset's update is a merge or nothing. Its "new copy" would be a whole
240
+ * new dataset, which consumes org quota and lands empty: exactly what the
241
+ * install already does, with the quota check an update does not have.
242
+ */ export async function locateInstalledDatasetSchema(request) {
243
+ var _doc_get, _doc_get1, _ref, _ref1, _doc_get2, _doc_get3;
244
+ if (!request.orgId) return refuse(404, 'Site has no owning organization');
245
+ const datasets = await datasetsOf(request.orgId).get();
246
+ const doc = datasets.docs.find((entry)=>entry.get('source.listingId') === request.listingId && !entry.get('deletedAt'));
247
+ if (!doc) return refuse(404, 'Not installed in this organization');
248
+ const byLabel = {};
249
+ for (const entry of datasets.docs){
250
+ var _entry_get;
251
+ const label = String((_entry_get = entry.get('displayName')) != null ? _entry_get : '').toLowerCase();
252
+ if (label && !byLabel[label]) byLabel[label] = entry.id;
253
+ }
254
+ const { schema } = resolveInstalledDatasetSchema(request.published, byLabel);
255
+ const current = (_doc_get = doc.get('model')) != null ? _doc_get : {
256
+ order: (_doc_get1 = doc.get('fields')) != null ? _doc_get1 : [],
257
+ fields: {}
258
+ };
259
+ const summary = summarizeSchemaChange(current, schema);
260
+ // The count is read for the preview, not estimated: "3 fields will be
261
+ // removed" means nothing without "from 1,240 records".
262
+ const records = await doc.ref.collection('records').count().get().catch(()=>null);
263
+ const recordCount = (_ref = records == null ? void 0 : records.data().count) != null ? _ref : 0;
264
+ return {
265
+ ok: true,
266
+ current,
267
+ incoming: schema,
268
+ installedVersion: String((_ref1 = (_doc_get2 = doc.get('installedFrom.version')) != null ? _doc_get2 : doc.get('source.version')) != null ? _ref1 : '') || null,
269
+ baseSha: (_doc_get3 = doc.get('installedFrom.sha256')) != null ? _doc_get3 : null,
270
+ // A schema change that removes or retypes a field reinterprets rows that
271
+ // already exist. It is applicable, but never without the caller having
272
+ // been told the count and said yes to it specifically.
273
+ impact: {
274
+ preview: {
275
+ schema: {
276
+ added: summary.added,
277
+ removed: summary.removed,
278
+ retyped: summary.retyped,
279
+ additiveOnly: summary.additiveOnly,
280
+ recordCount
281
+ }
282
+ },
283
+ destructive: !summary.additiveOnly,
284
+ refusal: `This update removes or retypes ${summary.removed.length + summary.retyped.length} field(s) on a dataset holding ${recordCount} record(s).`
285
+ },
286
+ apply: async ({ content, stamp })=>{
287
+ var _ref;
288
+ await doc.ref.set({
289
+ model: content,
290
+ // The v1 flat list the older editor still reads, kept in step.
291
+ fields: (_ref = content == null ? void 0 : content.order) != null ? _ref : [],
292
+ installedFrom: stamp.installedFrom,
293
+ source: stamp.source,
294
+ updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
295
+ }, {
296
+ merge: true
297
+ });
298
+ }
299
+ };
300
+ }
301
+ /** The four answers, as the owner registers them. */ export const datasetSchemaArtifactOwner = {
302
+ snapshot: snapshotDatasetSchema,
303
+ admits: admitsDatasetSchema,
304
+ prepare: prepareDatasetSchemaInstall,
305
+ locate: locateInstalledDatasetSchema
306
+ };
307
+
308
+ //# sourceMappingURL=dataset-schema-artifact.server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/artifact/dataset-schema-artifact.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkDatasetQuota,\n checkEntitlement,\n createResourceUid,\n defaultScopeForNewResource,\n newResourceScopeFields,\n scopeCovers,\n} from '@aglyn/aglyn/server'\nimport type {\n ArtifactInstallRequest,\n ArtifactLocateRequest,\n ArtifactRefusal,\n ArtifactSnapshot,\n ArtifactSnapshotRequest,\n ArtifactWorkspace,\n InstalledArtifactCopy,\n PluginArtifactOwner,\n PreparedArtifactInstall,\n} from '@aglyn/aglyn/plugin-manager/plugin-artifact-types'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n resolveInstalledDatasetSchema,\n sanitizeDatasetSchema,\n summarizeSchemaChange,\n type PublishedDatasetSchema,\n} from './dataset-schema'\n\n/**\n * A dataset's schema as an installable artifact (AGL-657), answered by the\n * plugin that keeps datasets (AGL-3080).\n *\n * The installer (the marketplace) owns the listing, who may act on it, the\n * purchase, the provenance stamp and the tally, and asks these four answers\n * for everything that touches `orgs/{orgId}/datasets`, which it never reads.\n * Registered from `declarations.console-server.ts` and loaded with the first\n * publish, install or update.\n */\n\nconst refuse = (status: number, error: string): ArtifactRefusal => ({\n ok: false,\n status,\n error,\n})\n\nconst datasetsOf = (orgId: string) =>\n firebaseAdmin.app().firestore().collection('orgs').doc(orgId).collection('datasets')\n\nconst overDatasetQuota = (limit: number): ArtifactRefusal =>\n refuse(403, `Dataset limit reached (${limit}) — see Billing to upgrade.`)\n\n/**\n * PUBLISHING: an org dataset's SCHEMA, reduced to what travels.\n *\n * Datasets live at `orgs/{orgId}/datasets/{id}` and are org-shared (AGL-237),\n * so the source is named by the publishing org and the dataset's id.\n *\n * Records NEVER travel: `sanitizeDatasetSchema` reads the model, and nothing\n * here touches the `records` subcollection. A dataset's rows are the org's\n * customer data; only the shape is publishable.\n */\nexport async function snapshotDatasetSchema(\n request: ArtifactSnapshotRequest,\n): Promise<ArtifactSnapshot | ArtifactRefusal> {\n const datasets = datasetsOf(request.orgId)\n const datasetSnapshot = await datasets.doc(request.sourceId).get()\n const dataset = datasetSnapshot.data() as any\n if (!datasetSnapshot.exists || dataset?.deletedAt) {\n return refuse(404, 'Unknown dataset')\n }\n // v1 datasets carry a flat `fields: string[]` with no model; publish the\n // derived shape so unmigrated datasets are publishable too (AGL-102 shim,\n // inlined rather than importing core's deriveModelFromFields to keep the\n // sanitizer's input one plain shape).\n const model = dataset?.model?.fields\n ? dataset.model\n : {\n fields: Object.fromEntries(\n (Array.isArray(dataset?.fields) ? dataset.fields : []).map(\n (name: unknown) => [String(name), { name: String(name), type: 'text' }],\n ),\n ),\n order: (Array.isArray(dataset?.fields) ? dataset.fields : []).map(String),\n }\n\n const sanitized = sanitizeDatasetSchema(model)\n if (sanitized.ok === false) return refuse(422, sanitized.error)\n\n // Reference fields name a dataset id that means nothing outside this org,\n // so resolve each target's display name now: the installer relinks by that\n // label (see resolveInstalledDatasetSchema).\n const referenced = new Set(\n Object.values(sanitized.schema.fields)\n .map((field) => field.reference?.datasetId)\n .filter((id): id is string => Boolean(id)),\n )\n if (referenced.size) {\n const labels = new Map<string, string>()\n await Promise.all(\n [...referenced].map(async (id) => {\n const snapshot = await datasets.doc(id).get()\n const label = snapshot.get('displayName')\n if (label) labels.set(id, String(label))\n }),\n )\n for (const field of Object.values(sanitized.schema.fields)) {\n const target = field.reference?.datasetId\n if (target && labels.has(target)) {\n field.reference = {\n ...field.reference,\n datasetLabel: labels.get(target),\n }\n }\n }\n }\n return {\n ok: true,\n content: sanitized.schema,\n facts: { fieldCount: sanitized.schema.order.length },\n }\n}\n\n/** INSTALLING, before the listing is read: datasets are a plan feature. */\nexport async function admitsDatasetSchema(\n workspace: ArtifactWorkspace,\n): Promise<ArtifactRefusal | null> {\n if (!checkEntitlement(workspace.org as any, 'dataStore')) {\n return refuse(403, 'Datasets require a Starter plan or higher')\n }\n return null\n}\n\n/**\n * INSTALLING: a NEW, EMPTY dataset made from the published model.\n *\n * Installing never merges into an existing dataset, because a schema change\n * on a dataset that already holds rows would silently reinterpret live data.\n * Re-installing makes another dataset rather than replacing one, for the same\n * reason.\n *\n * Nothing is written until `commit`, which the installer calls once it has\n * recorded the provenance of exactly the content prepared here.\n */\nexport async function prepareDatasetSchemaInstall(\n request: ArtifactInstallRequest,\n): Promise<PreparedArtifactInstall | ArtifactRefusal> {\n const published = request.published as PublishedDatasetSchema | undefined\n if (!published?.order?.length) {\n return refuse(500, 'Dataset schema version missing')\n }\n const org = request.org as any\n const datasetsRef = datasetsOf(request.orgId)\n\n // Creating a dataset consumes org quota exactly like the console's own\n // create path (AGL-473): installing must not be a way around it.\n // Deliberately counts EVERY dataset the org owns, not the visible ones\n // (AGL-1046). Scoping decides who can see a dataset, never who pays for\n // it: the org owns all of them. Counting per scope would also make a\n // collaborator's remaining quota disagree with the admin's.\n //\n // This is the early answer, so a workspace already at its cap is refused\n // before the installer records any provenance. The one that holds is in\n // `commit`, below.\n const datasets = await datasetsRef.get()\n const quota = checkDatasetQuota(org, datasets.size)\n if (!quota.allowed) return overDatasetQuota(quota.limit)\n\n /**\n * Who the new dataset is shared with. Decided before relinking, because\n * it also decides what a reference field may point at.\n *\n * A dataset schema installs at ORGANIZATION scope and nowhere else\n * (`INSTALL_TARGETS.datasetSchema`), from the organization Marketplace, the\n * one surface that installs anything. The site that surface acts THROUGH\n * only finds the org: the org's first site, not a site anyone is working\n * in, and it never reaches here. So no site is in context, and the dataset\n * starts where a create on an organization page starts: All sites, whatever\n * the org's Default sharing says. Honoring the acting site instead would\n * hide the dataset from every other site, under an install dialog that\n * says it lands on \"the whole organization — every site\" (AGL-2891).\n */\n const visibleTo = defaultScopeForNewResource({\n defaultResourceScope: org?.defaultResourceScope,\n hostId: null,\n })\n\n // Relink reference fields onto this org's datasets by display name; what\n // can't be relinked degrades to text and is reported to the installer.\n // A candidate has to be visible everywhere the new dataset is, which is\n // the rule every reference answers to (`scopeCovers`, AGL-1044): one\n // pointing at a dataset some of those sites cannot see resolves to nothing\n // there, and nothing says why. That is also what keeps an install from\n // binding its reference fields to an agency's internal dataset of the same\n // name (AGL-1046): the display-name collision AGL-1039 fixed on the render\n // path, arriving here instead.\n const byLabel: Record<string, string> = {}\n for (const entry of datasets.docs) {\n if (!scopeCovers(entry.get('visibleTo'), visibleTo)) continue\n const label = String(entry.get('displayName') ?? '').toLowerCase()\n if (label && !byLabel[label]) byLabel[label] = entry.id\n }\n const { schema, degradedFieldIds } = resolveInstalledDatasetSchema(\n published,\n byLabel,\n )\n const datasetId = createResourceUid()\n const { listing } = request\n\n return {\n ok: true,\n // The RELINKED schema, not the published one, is what the base snapshot\n // records (AGL-1015): relinking rewrites reference fields onto this org's\n // datasets, so a base holding the publisher's ids would report every\n // relinked field as a user edit the moment anything is diffed.\n content: schema,\n /**\n * THE ENFORCEMENT POINT (AGL-3454, the AGL-2371 treatment): the count,\n * the decision and the create in ONE transaction.\n *\n * The count above is taken before the installer awaits the provenance\n * write, and every await is a yield: N installs in flight each read the\n * same pre-count, each find room, and each land, and nothing re-counts\n * afterwards. `tx.get` on the aggregate takes a pessimistic lock on the\n * documents the query matched, so the loser of a race retries, re-reads\n * the higher count and is refused, having written nothing.\n *\n * The org document is read outside, as the console's create path reads\n * it: the plan and the purchased add-ons are not client-writable, so\n * locking it would buy contention rather than correctness.\n */\n commit: async (stamp) => {\n const refused = await firebaseAdmin\n .app()\n .firestore()\n .runTransaction<ArtifactRefusal | null>(async (tx) => {\n const live = (await tx.get(datasetsRef.count())).data().count\n const authoritative = checkDatasetQuota(org, live)\n if (!authoritative.allowed) return overDatasetQuota(authoritative.limit)\n tx.create(datasetsRef.doc(datasetId), {\n displayName: String(listing.displayName ?? 'Dataset').slice(0, 120),\n ...(listing.description ? { description: listing.description } : {}),\n // `fields` is the v1 flat list the older editor still reads; keep it\n // in step with the model so both readers agree (AGL-102).\n fields: schema.order,\n model: schema,\n source: stamp.source,\n installedFrom: stamp.installedFrom,\n // The scope decided above. Stamping it is not optional: a dataset\n // with no `visibleTo` matches no scoped read and would render on no\n // site at all.\n //\n // Through the AGL-1478 gate since AGL-1484, so \"every other dataset\n // creator\" is a fact about the type rather than about four object\n // literals that happen to agree today.\n ...newResourceScopeFields(visibleTo),\n createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n return null\n })\n if (refused) return refused\n return {\n ok: true,\n report: { datasetId, fields: schema.order.length, degradedFieldIds },\n }\n },\n }\n}\n\n/**\n * UPDATING: the dataset a listing installed in the site's organization, and\n * what taking the version on offer would do to its records (AGL-1018).\n *\n * The incoming schema is relinked onto THIS org's datasets exactly as install\n * does. The base holds the relinked shape, so diffing the published one would\n * report every reference field as a user edit.\n *\n * A dataset's update is a merge or nothing. Its \"new copy\" would be a whole\n * new dataset, which consumes org quota and lands empty: exactly what the\n * install already does, with the quota check an update does not have.\n */\nexport async function locateInstalledDatasetSchema(\n request: ArtifactLocateRequest,\n): Promise<InstalledArtifactCopy | ArtifactRefusal> {\n if (!request.orgId) return refuse(404, 'Site has no owning organization')\n const datasets = await datasetsOf(request.orgId).get()\n const doc = datasets.docs.find(\n (entry) =>\n entry.get('source.listingId') === request.listingId && !entry.get('deletedAt'),\n )\n if (!doc) return refuse(404, 'Not installed in this organization')\n const byLabel: Record<string, string> = {}\n for (const entry of datasets.docs) {\n const label = String(entry.get('displayName') ?? '').toLowerCase()\n if (label && !byLabel[label]) byLabel[label] = entry.id\n }\n const { schema } = resolveInstalledDatasetSchema(\n request.published as PublishedDatasetSchema,\n byLabel,\n )\n const current = doc.get('model') ?? { order: doc.get('fields') ?? [], fields: {} }\n const summary = summarizeSchemaChange(current as any, schema as any)\n // The count is read for the preview, not estimated: \"3 fields will be\n // removed\" means nothing without \"from 1,240 records\".\n const records = await doc.ref\n .collection('records')\n .count()\n .get()\n .catch(() => null)\n const recordCount = records?.data().count ?? 0\n return {\n ok: true,\n current,\n incoming: schema,\n installedVersion:\n String(doc.get('installedFrom.version') ?? doc.get('source.version') ?? '') || null,\n baseSha: doc.get('installedFrom.sha256') ?? null,\n // A schema change that removes or retypes a field reinterprets rows that\n // already exist. It is applicable, but never without the caller having\n // been told the count and said yes to it specifically.\n impact: {\n preview: {\n schema: {\n added: summary.added,\n removed: summary.removed,\n retyped: summary.retyped,\n additiveOnly: summary.additiveOnly,\n recordCount,\n },\n },\n destructive: !summary.additiveOnly,\n refusal:\n `This update removes or retypes ${\n summary.removed.length + summary.retyped.length\n } field(s) on a dataset holding ${recordCount} record(s).`,\n },\n apply: async ({ content, stamp }) => {\n await doc.ref.set(\n {\n model: content,\n // The v1 flat list the older editor still reads, kept in step.\n fields: (content as PublishedDatasetSchema | undefined)?.order ?? [],\n installedFrom: stamp.installedFrom,\n source: stamp.source,\n updatedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n },\n }\n}\n\n/** The four answers, as the owner registers them. */\nexport const datasetSchemaArtifactOwner: PluginArtifactOwner = {\n snapshot: snapshotDatasetSchema,\n admits: admitsDatasetSchema,\n prepare: prepareDatasetSchemaInstall,\n locate: locateInstalledDatasetSchema,\n}\n"],"names":["checkDatasetQuota","checkEntitlement","createResourceUid","defaultScopeForNewResource","newResourceScopeFields","scopeCovers","firebaseAdmin","resolveInstalledDatasetSchema","sanitizeDatasetSchema","summarizeSchemaChange","refuse","status","error","ok","datasetsOf","orgId","app","firestore","collection","doc","overDatasetQuota","limit","snapshotDatasetSchema","request","dataset","datasets","datasetSnapshot","sourceId","get","data","exists","deletedAt","model","fields","Object","fromEntries","Array","isArray","map","name","String","type","order","sanitized","referenced","Set","values","schema","field","reference","datasetId","filter","id","Boolean","size","labels","Map","Promise","all","snapshot","label","set","target","has","datasetLabel","content","facts","fieldCount","length","admitsDatasetSchema","workspace","org","prepareDatasetSchemaInstall","published","datasetsRef","quota","allowed","visibleTo","defaultResourceScope","hostId","byLabel","entry","docs","toLowerCase","degradedFieldIds","listing","commit","stamp","refused","runTransaction","tx","live","count","authoritative","create","displayName","slice","description","source","installedFrom","createdAt","FieldValue","serverTimestamp","report","locateInstalledDatasetSchema","find","listingId","current","summary","records","ref","catch","recordCount","incoming","installedVersion","baseSha","impact","preview","added","removed","retyped","additiveOnly","destructive","refusal","apply","updatedAt","merge","datasetSchemaArtifactOwner","admits","prepare","locate"],"mappings":"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"}