@aglyn/plugins-data 1.0.0-beta.221 → 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.
- package/README.md +9 -6
- package/package.json +10 -10
- package/src/index.d.ts +1 -1
- package/src/index.js +1 -1
- package/src/index.js.map +1 -1
- package/src/lib/artifact/dataset-schema-artifact.server.d.ts +57 -0
- package/src/lib/artifact/dataset-schema-artifact.server.js +308 -0
- package/src/lib/artifact/dataset-schema-artifact.server.js.map +1 -0
- package/src/lib/artifact/dataset-schema.d.ts +136 -0
- package/src/lib/artifact/dataset-schema.js +232 -0
- package/src/lib/artifact/dataset-schema.js.map +1 -0
- package/src/lib/components/dataset-record-dialog.component.d.ts +1 -1
- package/src/lib/components/dataset-record-dialog.component.js +1 -1
- package/src/lib/components/dataset-record-dialog.component.js.map +1 -1
- package/src/lib/components/dataset-record-filter.d.ts +1 -1
- package/src/lib/components/dataset-record-filter.js +2 -2
- package/src/lib/components/dataset-record-filter.js.map +1 -1
- package/src/lib/components/dataset-schema-dialog.component.d.ts +1 -1
- package/src/lib/components/dataset-schema-dialog.component.js +55 -23
- package/src/lib/components/dataset-schema-dialog.component.js.map +1 -1
- package/src/lib/components/host-datasets-card.component.js +91 -403
- package/src/lib/components/host-datasets-card.component.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +12 -0
- package/src/lib/constants/bundle-common.js +13 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +16 -1
- package/src/lib/declarations.console-server.js +34 -2
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.server.d.ts +13 -0
- package/src/lib/declarations.server.js +48 -2
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/form-target/dataset-form-record-target.server.js +5 -3
- package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -1
- package/src/lib/model/dataset-models.d.ts +392 -0
- package/src/lib/model/dataset-models.js +806 -0
- package/src/lib/model/dataset-models.js.map +1 -0
- package/src/lib/model/dataset-record-view.d.ts +1 -1
- package/src/lib/model/dataset-record-view.js +1 -2
- package/src/lib/model/dataset-record-view.js.map +1 -1
- package/src/lib/model/dataset-record.js +2 -2
- package/src/lib/model/dataset-record.js.map +1 -1
- package/src/lib/model/datasets.d.ts +162 -0
- package/src/lib/model/datasets.js +227 -0
- package/src/lib/model/datasets.js.map +1 -0
- package/src/lib/plugin.d.ts +4 -0
- package/src/lib/plugin.js +40 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/record-pages/record-page-live-paths.server.d.ts +55 -0
- package/src/lib/record-pages/record-page-live-paths.server.js +83 -0
- package/src/lib/record-pages/record-page-live-paths.server.js.map +1 -0
- package/src/lib/record-pages/record-page-read.server.d.ts +70 -0
- package/src/lib/record-pages/record-page-read.server.js +175 -0
- package/src/lib/record-pages/record-page-read.server.js.map +1 -0
- package/src/lib/record-pages/record-page-resolver.server.d.ts +37 -0
- package/src/lib/record-pages/record-page-resolver.server.js +131 -0
- package/src/lib/record-pages/record-page-resolver.server.js.map +1 -0
- package/src/lib/record-pages/record-page-sitemap.server.d.ts +32 -0
- package/src/lib/record-pages/record-page-sitemap.server.js +102 -0
- package/src/lib/record-pages/record-page-sitemap.server.js.map +1 -0
- package/src/lib/record-pages/record-page-source.d.ts +42 -0
- package/src/lib/record-pages/record-page-source.js +176 -0
- package/src/lib/record-pages/record-page-source.js.map +1 -0
- package/src/lib/record-pages/record-pages-route.d.ts +42 -0
- package/src/lib/record-pages/record-pages-route.js +348 -0
- package/src/lib/record-pages/record-pages-route.js.map +1 -0
- package/src/lib/record-pages/record-pages.d.ts +218 -0
- package/src/lib/record-pages/record-pages.js +351 -0
- package/src/lib/record-pages/record-pages.js.map +1 -0
- package/src/lib/record-pages/record-template-row-chip.component.d.ts +28 -0
- package/src/lib/record-pages/record-template-row-chip.component.js +87 -0
- package/src/lib/record-pages/record-template-row-chip.component.js.map +1 -0
- package/src/lib/record-pages/record-template-section.component.d.ts +30 -0
- package/src/lib/record-pages/record-template-section.component.js +491 -0
- package/src/lib/record-pages/record-template-section.component.js.map +1 -0
- package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -1
- package/src/lib/repeat/dataset-repeat-rows.js +30 -12
- package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
- package/src/lib/repeat/dataset-repeat-rows.server.d.ts +1 -1
- package/src/lib/repeat/dataset-repeat-rows.server.js +23 -12
- package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -1
- package/src/lib/repeat/repeat-record-pages.d.ts +48 -0
- package/src/lib/repeat/repeat-record-pages.js +64 -0
- package/src/lib/repeat/repeat-record-pages.js.map +1 -0
- package/src/lib/server/announce-dataset-records.d.ts +1 -1
- package/src/lib/server/announce-dataset-records.js +1 -1
- package/src/lib/server/announce-dataset-records.js.map +1 -1
- package/src/lib/server/api-v1/datasets.js +5 -3
- package/src/lib/server/api-v1/datasets.js.map +1 -1
- package/src/lib/server/dataset-figures.js +2 -2
- package/src/lib/server/dataset-figures.js.map +1 -1
- package/src/lib/server/dataset-live-pages.d.ts +147 -0
- package/src/lib/server/dataset-live-pages.js +436 -0
- package/src/lib/server/dataset-live-pages.js.map +1 -0
- package/src/lib/server/dataset-steps.server.d.ts +45 -0
- package/src/lib/server/dataset-steps.server.js +268 -0
- package/src/lib/server/dataset-steps.server.js.map +1 -0
- package/src/lib/server/datasets-route.d.ts +2 -0
- package/src/lib/server/datasets-route.js +102 -6
- package/src/lib/server/datasets-route.js.map +1 -1
- package/src/lib/server/resolve-dataset.d.ts +40 -0
- package/src/lib/server/resolve-dataset.js +64 -0
- package/src/lib/server/resolve-dataset.js.map +1 -0
- package/src/lib/server.d.ts +3 -2
- package/src/lib/server.js +8 -9
- package/src/lib/server.js.map +1 -1
- package/src/lib/site-bundle/datasets-package.d.ts +42 -0
- package/src/lib/site-bundle/datasets-package.js +95 -0
- package/src/lib/site-bundle/datasets-package.js.map +1 -0
- package/src/lib/site-bundle/datasets-site-bundle.server.js +2 -2
- package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -1
- package/src/lib/{model/index.js → tenant-api.d.ts} +12 -7
- package/src/lib/tenant-api.js +30 -0
- package/src/lib/tenant-api.js.map +1 -0
- package/src/lib/transfer/dataset-transfer-key.d.ts +30 -0
- package/src/lib/transfer/dataset-transfer-key.js +31 -0
- package/src/lib/transfer/dataset-transfer-key.js.map +1 -0
- package/src/lib/transfer/dataset-transfer-model.d.ts +146 -0
- package/src/lib/transfer/dataset-transfer-model.js +536 -0
- package/src/lib/transfer/dataset-transfer-model.js.map +1 -0
- package/src/lib/{model/index.d.ts → transfer/dataset-transfer-resource.d.ts} +2 -6
- package/src/lib/transfer/dataset-transfer-resource.js +41 -0
- package/src/lib/transfer/dataset-transfer-resource.js.map +1 -0
- package/src/lib/transfer/dataset-transfer.server.d.ts +69 -0
- package/src/lib/transfer/dataset-transfer.server.js +930 -0
- package/src/lib/transfer/dataset-transfer.server.js.map +1 -0
- package/src/lib/model/dataset-csv.d.ts +0 -36
- package/src/lib/model/dataset-csv.js +0 -85
- package/src/lib/model/dataset-csv.js.map +0 -1
- package/src/lib/model/dataset-io.d.ts +0 -44
- package/src/lib/model/dataset-io.js +0 -85
- package/src/lib/model/dataset-io.js.map +0 -1
- package/src/lib/model/index.js.map +0 -1
- package/src/lib/server/datasets-export-route.d.ts +0 -57
- package/src/lib/server/datasets-export-route.js +0 -229
- 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
|
|
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
|
|
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 {
|
|
54
|
+
import { useTransferLauncher } from '@aglyn/aglyn'
|
|
55
|
+
import { datasetTransferResourceKey } from '@aglyn/plugins-data'
|
|
51
56
|
|
|
52
|
-
|
|
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.
|
|
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.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
34
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
35
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
36
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
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
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/
|
|
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/
|
|
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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"}
|