@aglyn/plugins-data 1.0.0-beta.238 → 1.0.0-beta.240

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-data",
3
- "version": "1.0.0-beta.238",
3
+ "version": "1.0.0-beta.240",
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.238",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.238",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.238",
31
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.238",
32
- "@aglyn/shared-util-http": "1.0.0-beta.238",
33
- "@aglyn/shared-util-timestamp": "1.0.0-beta.238",
34
- "@aglyn/tenant-data-admin": "1.0.0-beta.238",
35
- "@aglyn/tenant-feature-instance": "1.0.0-beta.238",
36
- "@aglyn/tenant-runtime": "1.0.0-beta.238",
28
+ "@aglyn/aglyn": "1.0.0-beta.240",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.240",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.240",
31
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.240",
32
+ "@aglyn/shared-util-http": "1.0.0-beta.240",
33
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.240",
34
+ "@aglyn/tenant-data-admin": "1.0.0-beta.240",
35
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.240",
36
+ "@aglyn/tenant-runtime": "1.0.0-beta.240",
37
37
  "@swc/helpers": "0.5.23"
38
38
  },
39
39
  "peerDependencies": {
@@ -0,0 +1,22 @@
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
+ /**
18
+ * Called from the console registrar. The owner is named rather than read off
19
+ * the loader's marker, so the route registers the same way under a spec that
20
+ * calls the registrar directly.
21
+ */
22
+ export declare function registerDataRecordRoutes(): void;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
17
+ import { registerPluginRecordRoute } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
18
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
19
+ /**
20
+ * Where a dataset is read, published for every other surface (AGL-3616).
21
+ *
22
+ * An assistant job that made a menu or a team as a dataset opens it on the
23
+ * Data page, which is this plugin's — so it asks the record-route registry
24
+ * for `dataset` rather than spelling this plugin's nav slug, and gets `null`
25
+ * (text instead of a link) where it is not loaded. A dataset belongs to the
26
+ * organization and is shared with sites: a site's Data page lists the ones it
27
+ * can see, and the organization's lists them all. Neither has a page of one
28
+ * dataset, so a record opens the list.
29
+ */ /** The nav slug the shell resolves this plugin's site page by. */ const DATA_SLUG = 'data';
30
+ function list({ orgSlug, host }) {
31
+ if (!orgSlug) return null;
32
+ return host ? buildRoute(Route.HOST_PLUGIN, {
33
+ orgSlug,
34
+ host,
35
+ pluginSlug: DATA_SLUG
36
+ }) : buildRoute(Route.ORG_DATA, {
37
+ orgSlug
38
+ });
39
+ }
40
+ /**
41
+ * Called from the console registrar. The owner is named rather than read off
42
+ * the loader's marker, so the route registers the same way under a spec that
43
+ * calls the registrar directly.
44
+ */ export function registerDataRecordRoutes() {
45
+ registerPluginRecordRoute('dataset', {
46
+ list,
47
+ record: (context)=>list(context)
48
+ }, {
49
+ pluginId: BUNDLE_ID
50
+ });
51
+ }
52
+
53
+ //# sourceMappingURL=data-record-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/data-record-routes.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 { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n registerPluginRecordRoute,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * Where a dataset is read, published for every other surface (AGL-3616).\n *\n * An assistant job that made a menu or a team as a dataset opens it on the\n * Data page, which is this plugin's — so it asks the record-route registry\n * for `dataset` rather than spelling this plugin's nav slug, and gets `null`\n * (text instead of a link) where it is not loaded. A dataset belongs to the\n * organization and is shared with sites: a site's Data page lists the ones it\n * can see, and the organization's lists them all. Neither has a page of one\n * dataset, so a record opens the list.\n */\n\n/** The nav slug the shell resolves this plugin's site page by. */\nconst DATA_SLUG = 'data'\n\nfunction list({ orgSlug, host }: PluginRecordRouteContext): string | null {\n if (!orgSlug) return null\n return host ? buildRoute(Route.HOST_PLUGIN, { orgSlug, host, pluginSlug: DATA_SLUG }) : buildRoute(Route.ORG_DATA, { orgSlug })\n}\n\n/**\n * Called from the console registrar. The owner is named rather than read off\n * the loader's marker, so the route registers the same way under a spec that\n * calls the registrar directly.\n */\nexport function registerDataRecordRoutes(): void {\n registerPluginRecordRoute('dataset', { list, record: (context) => list(context) }, { pluginId: BUNDLE_ID })\n}\n"],"names":["buildRoute","Route","registerPluginRecordRoute","BUNDLE_ID","DATA_SLUG","list","orgSlug","host","HOST_PLUGIN","pluginSlug","ORG_DATA","registerDataRecordRoutes","record","context","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SACEC,yBAAyB,QAEpB,mDAAkD;AACzD,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;;;;;;;CAUC,GAED,gEAAgE,GAChE,MAAMC,YAAY;AAElB,SAASC,KAAK,EAAEC,OAAO,EAAEC,IAAI,EAA4B;IACvD,IAAI,CAACD,SAAS,OAAO;IACrB,OAAOC,OAAOP,WAAWC,MAAMO,WAAW,EAAE;QAAEF;QAASC;QAAME,YAAYL;IAAU,KAAKJ,WAAWC,MAAMS,QAAQ,EAAE;QAAEJ;IAAQ;AAC/H;AAEA;;;;CAIC,GACD,OAAO,SAASK;IACdT,0BAA0B,WAAW;QAAEG;QAAMO,QAAQ,CAACC,UAAYR,KAAKQ;IAAS,GAAG;QAAEC,UAAUX;IAAU;AAC3G"}
package/src/lib/plugin.js CHANGED
@@ -26,6 +26,7 @@ import { DATASET_TRANSFER_RESOURCE } from "./transfer/dataset-transfer-key.js";
26
26
  import { DATASET_REPEAT_SOURCE } from "./repeat/dataset-repeat-source.js";
27
27
  import { RECORD_PAGE_ADDRESS_FIELD } from "./record-pages/record-pages.js";
28
28
  import { RECORD_PAGE_SOURCE } from "./record-pages/record-page-source.js";
29
+ import { registerDataRecordRoutes } from "./model/data-record-routes.js";
29
30
  import RecordTemplateRowChip from "./record-pages/record-template-row-chip.component.js";
30
31
  import RecordTemplateSection from "./record-pages/record-template-section.component.js";
31
32
  /** Code-split: the Data console page only loads when opened. */ const DataConsolePage = lazy(()=>import("./components/data-console-page.js"));
@@ -57,6 +58,9 @@ import RecordTemplateSection from "./record-pages/record-template-section.compon
57
58
  registerPageRecordSource(RECORD_PAGE_SOURCE);
58
59
  // The workspace's datasets, for another plugin's picker (AGL-3080).
59
60
  registerDatasetRecordList();
61
+ // …and where one is read, for a surface that links it (AGL-3616): an AI
62
+ // job's dataset opens on the Data page.
63
+ registerDataRecordRoutes();
60
64
  // A dataset's records in the import wizard and the export dialog
61
65
  // (AGL-3530); the Data card names the dataset when it opens them.
62
66
  registerPluginTransferResourceUi(DATASET_TRANSFER_RESOURCE, // The Import & export hub lists each dataset from the `dataset` record list (AGL-3535).
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/data/src/lib/plugin.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 HostDatasetsCard from './components/host-datasets-card.component'\nimport * as Aglyn from '@aglyn/aglyn'\nimport { registerRepeatSource } from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { mdiDatabaseOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerDatasetRecordList } from './model/dataset-record-list'\nimport { registerPageRecordSource } from '@aglyn/aglyn/app-utils/page-record-sources'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { DATASET_TRANSFER_RESOURCE } from './transfer/dataset-transfer-key'\nimport { DATASET_REPEAT_SOURCE } from './repeat/dataset-repeat-source'\nimport { RECORD_PAGE_ADDRESS_FIELD } from './record-pages/record-pages'\nimport { RECORD_PAGE_SOURCE } from './record-pages/record-page-source'\nimport RecordTemplateRowChip from './record-pages/record-template-row-chip.component'\nimport RecordTemplateSection from './record-pages/record-template-section.component'\n\n/** Code-split: the Data console page only loads when opened. */\nconst DataConsolePage = lazy(() => import('./components/data-console-page'))\n\n/**\n * Data feature plugin (AGL-395). Console-only — datasets are org-shared\n * document collections consumed by repeatable components at render time\n * through binding resolution, not a canvas element of their own, so there\n * is no UI bundle. The console half declares the host Data nav + page\n * through the ConsoleExtension registry, gated by the `dataStore`\n * entitlement. The org Data page (`Route.ORG_DATA`) is an org-scoped app route\n * that imports {@link HostDatasetsCard} directly.\n *\n * Record pages (AGL-3475) add a section to the besigner's Page Properties, a\n * chip to the Pages list's rows, and the page-record source that draws a\n * record template on the canvas for one of its records.\n *\n * It also registers datasets as a repeat source (AGL-3111), which is what puts\n * \"Repeat over dataset\" on every element's Attributes panel and draws a\n * repeat's copies from the real rows on the besigner canvas. Registered here,\n * inside the function the loader calls by name, and never by a module's own\n * load: this package declares no side effects, so a bundler deletes a module\n * imported only to run one (AGL-3025).\n */\nexport function registerDataConsole(): void {\n registerRepeatSource(DATASET_REPEAT_SOURCE)\n // The \"Page address\" field type (AGL-3475), for the schema dialog's type\n // list and the record editor's checks.\n Aglyn.registerCustomFieldType(RECORD_PAGE_ADDRESS_FIELD)\n // A record template draws on the canvas for one of its records (AGL-3475).\n registerPageRecordSource(RECORD_PAGE_SOURCE)\n // The workspace's datasets, for another plugin's picker (AGL-3080).\n registerDatasetRecordList()\n // A dataset's records in the import wizard and the export dialog\n // (AGL-3530); the Data card names the dataset when it opens them.\n registerPluginTransferResourceUi(\n DATASET_TRANSFER_RESOURCE,\n // The Import & export hub lists each dataset from the `dataset` record list (AGL-3535).\n { label: 'Dataset records', icon: { path: mdiDatabaseOutline.path }, instancesFrom: 'dataset' },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n // Org datasets card (AGL-419): org/data renders it through the\n // 'orgData' widget slot.\n widgets: [\n {\n slot: 'orgData',\n widgetId: 'data-org-datasets',\n Component: HostDatasetsCard,\n },\n // Record pages (AGL-3475): the template's settings in Page Properties,\n // and its chip in the Pages list.\n {\n slot: 'besignerPageProperties',\n widgetId: 'data-record-template',\n Component: RecordTemplateSection,\n },\n {\n slot: 'hostScreenRow',\n widgetId: 'data-record-template-chip',\n Component: RecordTemplateRowChip,\n },\n ],\n pluginId: BUNDLE_ID,\n displayName: 'Data',\n featureFlag: 'dataStore',\n navItems: [\n {\n label: 'Data',\n href: '/data',\n // Reuse the existing release-flag nav-tab so staff-preview gating is\n // unchanged now that the tab comes from the plugin (AGL-395).\n navTabId: 'nav-tab-data',\n icon: { path: mdiDatabaseOutline.path },\n header: {\n title: 'Data',\n icon: { path: mdiDatabaseOutline.path },\n docsTopic: 'datasets',\n },\n Component: DataConsolePage,\n },\n ],\n })\n}\n\nexport { default as HostDatasetsCard } from './components/host-datasets-card.component'\nexport type { HostDatasetsCardProps } from './components/host-datasets-card.component'\n"],"names":["HostDatasetsCard","Aglyn","registerRepeatSource","mdiDatabaseOutline","lazy","BUNDLE_ID","registerDatasetRecordList","registerPageRecordSource","registerPluginTransferResourceUi","DATASET_TRANSFER_RESOURCE","DATASET_REPEAT_SOURCE","RECORD_PAGE_ADDRESS_FIELD","RECORD_PAGE_SOURCE","RecordTemplateRowChip","RecordTemplateSection","DataConsolePage","registerDataConsole","registerCustomFieldType","label","icon","path","instancesFrom","pluginId","registerConsoleExtension","widgets","slot","widgetId","Component","displayName","featureFlag","navItems","href","navTabId","header","title","docsTopic","default"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,OAAOA,sBAAsB,+CAA2C;AACxE,YAAYC,WAAW,eAAc;AACrC,SAASC,oBAAoB,QAAQ,wCAAuC;AAC5E,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,wBAAwB,QAAQ,6CAA4C;AACrF,SAASC,gCAAgC,QAAQ,wDAAuD;AACxG,SAASC,yBAAyB,QAAQ,qCAAiC;AAC3E,SAASC,qBAAqB,QAAQ,oCAAgC;AACtE,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,kBAAkB,QAAQ,uCAAmC;AACtE,OAAOC,2BAA2B,uDAAmD;AACrF,OAAOC,2BAA2B,sDAAkD;AAEpF,8DAA8D,GAC9D,MAAMC,kBAAkBX,KAAK,IAAM,MAAM,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASY;IACdd,qBAAqBQ;IACrB,yEAAyE;IACzE,uCAAuC;IACvCT,MAAMgB,uBAAuB,CAACN;IAC9B,2EAA2E;IAC3EJ,yBAAyBK;IACzB,oEAAoE;IACpEN;IACA,iEAAiE;IACjE,kEAAkE;IAClEE,iCACEC,2BACA,wFAAwF;IACxF;QAAES,OAAO;QAAmBC,MAAM;YAAEC,MAAMjB,mBAAmBiB,IAAI;QAAC;QAAGC,eAAe;IAAU,GAC9F;QAAEC,UAAUjB;IAAU;IAExBJ,MAAMsB,wBAAwB,CAAC;QAC7B,+DAA+D;QAC/D,yBAAyB;QACzBC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,WAAW3B;YACb;YACA,uEAAuE;YACvE,kCAAkC;YAClC;gBACEyB,MAAM;gBACNC,UAAU;gBACVC,WAAWb;YACb;YACA;gBACEW,MAAM;gBACNC,UAAU;gBACVC,WAAWd;YACb;SACD;QACDS,UAAUjB;QACVuB,aAAa;QACbC,aAAa;QACbC,UAAU;YACR;gBACEZ,OAAO;gBACPa,MAAM;gBACN,qEAAqE;gBACrE,8DAA8D;gBAC9DC,UAAU;gBACVb,MAAM;oBAAEC,MAAMjB,mBAAmBiB,IAAI;gBAAC;gBACtCa,QAAQ;oBACNC,OAAO;oBACPf,MAAM;wBAAEC,MAAMjB,mBAAmBiB,IAAI;oBAAC;oBACtCe,WAAW;gBACb;gBACAR,WAAWZ;YACb;SACD;IACH;AACF;AAEA,SAASqB,WAAWpC,gBAAgB,QAAQ,+CAA2C"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/data/src/lib/plugin.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 HostDatasetsCard from './components/host-datasets-card.component'\nimport * as Aglyn from '@aglyn/aglyn'\nimport { registerRepeatSource } from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { mdiDatabaseOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerDatasetRecordList } from './model/dataset-record-list'\nimport { registerPageRecordSource } from '@aglyn/aglyn/app-utils/page-record-sources'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { DATASET_TRANSFER_RESOURCE } from './transfer/dataset-transfer-key'\nimport { DATASET_REPEAT_SOURCE } from './repeat/dataset-repeat-source'\nimport { RECORD_PAGE_ADDRESS_FIELD } from './record-pages/record-pages'\nimport { RECORD_PAGE_SOURCE } from './record-pages/record-page-source'\nimport { registerDataRecordRoutes } from './model/data-record-routes'\nimport RecordTemplateRowChip from './record-pages/record-template-row-chip.component'\nimport RecordTemplateSection from './record-pages/record-template-section.component'\n\n/** Code-split: the Data console page only loads when opened. */\nconst DataConsolePage = lazy(() => import('./components/data-console-page'))\n\n/**\n * Data feature plugin (AGL-395). Console-only — datasets are org-shared\n * document collections consumed by repeatable components at render time\n * through binding resolution, not a canvas element of their own, so there\n * is no UI bundle. The console half declares the host Data nav + page\n * through the ConsoleExtension registry, gated by the `dataStore`\n * entitlement. The org Data page (`Route.ORG_DATA`) is an org-scoped app route\n * that imports {@link HostDatasetsCard} directly.\n *\n * Record pages (AGL-3475) add a section to the besigner's Page Properties, a\n * chip to the Pages list's rows, and the page-record source that draws a\n * record template on the canvas for one of its records.\n *\n * It also registers datasets as a repeat source (AGL-3111), which is what puts\n * \"Repeat over dataset\" on every element's Attributes panel and draws a\n * repeat's copies from the real rows on the besigner canvas. Registered here,\n * inside the function the loader calls by name, and never by a module's own\n * load: this package declares no side effects, so a bundler deletes a module\n * imported only to run one (AGL-3025).\n */\nexport function registerDataConsole(): void {\n registerRepeatSource(DATASET_REPEAT_SOURCE)\n // The \"Page address\" field type (AGL-3475), for the schema dialog's type\n // list and the record editor's checks.\n Aglyn.registerCustomFieldType(RECORD_PAGE_ADDRESS_FIELD)\n // A record template draws on the canvas for one of its records (AGL-3475).\n registerPageRecordSource(RECORD_PAGE_SOURCE)\n // The workspace's datasets, for another plugin's picker (AGL-3080).\n registerDatasetRecordList()\n // …and where one is read, for a surface that links it (AGL-3616): an AI\n // job's dataset opens on the Data page.\n registerDataRecordRoutes()\n // A dataset's records in the import wizard and the export dialog\n // (AGL-3530); the Data card names the dataset when it opens them.\n registerPluginTransferResourceUi(\n DATASET_TRANSFER_RESOURCE,\n // The Import & export hub lists each dataset from the `dataset` record list (AGL-3535).\n { label: 'Dataset records', icon: { path: mdiDatabaseOutline.path }, instancesFrom: 'dataset' },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n // Org datasets card (AGL-419): org/data renders it through the\n // 'orgData' widget slot.\n widgets: [\n {\n slot: 'orgData',\n widgetId: 'data-org-datasets',\n Component: HostDatasetsCard,\n },\n // Record pages (AGL-3475): the template's settings in Page Properties,\n // and its chip in the Pages list.\n {\n slot: 'besignerPageProperties',\n widgetId: 'data-record-template',\n Component: RecordTemplateSection,\n },\n {\n slot: 'hostScreenRow',\n widgetId: 'data-record-template-chip',\n Component: RecordTemplateRowChip,\n },\n ],\n pluginId: BUNDLE_ID,\n displayName: 'Data',\n featureFlag: 'dataStore',\n navItems: [\n {\n label: 'Data',\n href: '/data',\n // Reuse the existing release-flag nav-tab so staff-preview gating is\n // unchanged now that the tab comes from the plugin (AGL-395).\n navTabId: 'nav-tab-data',\n icon: { path: mdiDatabaseOutline.path },\n header: {\n title: 'Data',\n icon: { path: mdiDatabaseOutline.path },\n docsTopic: 'datasets',\n },\n Component: DataConsolePage,\n },\n ],\n })\n}\n\nexport { default as HostDatasetsCard } from './components/host-datasets-card.component'\nexport type { HostDatasetsCardProps } from './components/host-datasets-card.component'\n"],"names":["HostDatasetsCard","Aglyn","registerRepeatSource","mdiDatabaseOutline","lazy","BUNDLE_ID","registerDatasetRecordList","registerPageRecordSource","registerPluginTransferResourceUi","DATASET_TRANSFER_RESOURCE","DATASET_REPEAT_SOURCE","RECORD_PAGE_ADDRESS_FIELD","RECORD_PAGE_SOURCE","registerDataRecordRoutes","RecordTemplateRowChip","RecordTemplateSection","DataConsolePage","registerDataConsole","registerCustomFieldType","label","icon","path","instancesFrom","pluginId","registerConsoleExtension","widgets","slot","widgetId","Component","displayName","featureFlag","navItems","href","navTabId","header","title","docsTopic","default"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,OAAOA,sBAAsB,+CAA2C;AACxE,YAAYC,WAAW,eAAc;AACrC,SAASC,oBAAoB,QAAQ,wCAAuC;AAC5E,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,wBAAwB,QAAQ,6CAA4C;AACrF,SAASC,gCAAgC,QAAQ,wDAAuD;AACxG,SAASC,yBAAyB,QAAQ,qCAAiC;AAC3E,SAASC,qBAAqB,QAAQ,oCAAgC;AACtE,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,kBAAkB,QAAQ,uCAAmC;AACtE,SAASC,wBAAwB,QAAQ,gCAA4B;AACrE,OAAOC,2BAA2B,uDAAmD;AACrF,OAAOC,2BAA2B,sDAAkD;AAEpF,8DAA8D,GAC9D,MAAMC,kBAAkBZ,KAAK,IAAM,MAAM,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASa;IACdf,qBAAqBQ;IACrB,yEAAyE;IACzE,uCAAuC;IACvCT,MAAMiB,uBAAuB,CAACP;IAC9B,2EAA2E;IAC3EJ,yBAAyBK;IACzB,oEAAoE;IACpEN;IACA,wEAAwE;IACxE,wCAAwC;IACxCO;IACA,iEAAiE;IACjE,kEAAkE;IAClEL,iCACEC,2BACA,wFAAwF;IACxF;QAAEU,OAAO;QAAmBC,MAAM;YAAEC,MAAMlB,mBAAmBkB,IAAI;QAAC;QAAGC,eAAe;IAAU,GAC9F;QAAEC,UAAUlB;IAAU;IAExBJ,MAAMuB,wBAAwB,CAAC;QAC7B,+DAA+D;QAC/D,yBAAyB;QACzBC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,WAAW5B;YACb;YACA,uEAAuE;YACvE,kCAAkC;YAClC;gBACE0B,MAAM;gBACNC,UAAU;gBACVC,WAAWb;YACb;YACA;gBACEW,MAAM;gBACNC,UAAU;gBACVC,WAAWd;YACb;SACD;QACDS,UAAUlB;QACVwB,aAAa;QACbC,aAAa;QACbC,UAAU;YACR;gBACEZ,OAAO;gBACPa,MAAM;gBACN,qEAAqE;gBACrE,8DAA8D;gBAC9DC,UAAU;gBACVb,MAAM;oBAAEC,MAAMlB,mBAAmBkB,IAAI;gBAAC;gBACtCa,QAAQ;oBACNC,OAAO;oBACPf,MAAM;wBAAEC,MAAMlB,mBAAmBkB,IAAI;oBAAC;oBACtCe,WAAW;gBACb;gBACAR,WAAWZ;YACb;SACD;IACH;AACF;AAEA,SAASqB,WAAWrC,gBAAgB,QAAQ,+CAA2C"}
@@ -0,0 +1,52 @@
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 PluginAiCapability, type PluginAiCapabilityArgs } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities';
18
+ import { type DatasetDraftFieldType } from './dataset-drafts';
19
+ /**
20
+ * WHAT AN AI BUILD CAN MAKE IN DATA (AGL-3616): a dataset with typed fields
21
+ * and its first records.
22
+ *
23
+ * The capability the AI plugin's build planner offers for "add a menu page"
24
+ * or "list our team": structured content kept as records rather than written
25
+ * into a page, so the owner edits one row and every page listing it follows.
26
+ * Its item is written by this plugin's `dataset` draft writer, so every rule
27
+ * is the writer's: `dataStore` (Starter and up), `datasetsPerOrg`,
28
+ * `release_data_store`, the member's role, and the model every record is
29
+ * held to.
30
+ *
31
+ * The arguments are flat, as the contract requires: each field as
32
+ * `Name:type`, and each record as its values in field order joined by ` | `,
33
+ * which `draftContent` turns into the writer's records. The planner fills
34
+ * them in, so no model runs (`estimateCredits` 0).
35
+ */
36
+ /** The operation's name in a build plan. */
37
+ export declare const DATASET_AI_OP = "dataset";
38
+ /** What separates one value of a record from the next in its argument. */
39
+ export declare const DATASET_AI_VALUE_SEPARATOR = " | ";
40
+ /** `"Price:number"` → `{ name: 'Price', type: 'number' }`; a type it does not know reads as text. */
41
+ export declare function datasetAiFieldOf(spec: string): {
42
+ name: string;
43
+ type: DatasetDraftFieldType;
44
+ };
45
+ /** The writer's content for an item. Pure. */
46
+ export declare function datasetDraftContentFromArgs(args: PluginAiCapabilityArgs): Readonly<Record<string, unknown>>;
47
+ export declare const datasetAiCapability: PluginAiCapability;
48
+ /**
49
+ * Registers the capability; the console surface calls it beside the writer,
50
+ * since only the console runs AI jobs (AGL-3026). Idempotent.
51
+ */
52
+ export declare function registerDatasetAiCapability(): void;
@@ -0,0 +1,145 @@
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 { registerPluginAiCapability } from "@aglyn/aglyn/plugin-manager/plugin-ai-capabilities";
18
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
19
+ import { DATASET_DRAFT_FIELD_NAME_MAX, DATASET_DRAFT_FIELD_TYPES, DATASET_DRAFT_FIELDS_MAX, DATASET_DRAFT_NAME_MAX, DATASET_DRAFT_RECORDS_MAX, DATASET_DRAFT_RESOURCE } from "./dataset-drafts.js";
20
+ /**
21
+ * WHAT AN AI BUILD CAN MAKE IN DATA (AGL-3616): a dataset with typed fields
22
+ * and its first records.
23
+ *
24
+ * The capability the AI plugin's build planner offers for "add a menu page"
25
+ * or "list our team": structured content kept as records rather than written
26
+ * into a page, so the owner edits one row and every page listing it follows.
27
+ * Its item is written by this plugin's `dataset` draft writer, so every rule
28
+ * is the writer's: `dataStore` (Starter and up), `datasetsPerOrg`,
29
+ * `release_data_store`, the member's role, and the model every record is
30
+ * held to.
31
+ *
32
+ * The arguments are flat, as the contract requires: each field as
33
+ * `Name:type`, and each record as its values in field order joined by ` | `,
34
+ * which `draftContent` turns into the writer's records. The planner fills
35
+ * them in, so no model runs (`estimateCredits` 0).
36
+ */ /** The operation's name in a build plan. */ export const DATASET_AI_OP = 'dataset';
37
+ /** What separates one value of a record from the next in its argument. */ export const DATASET_AI_VALUE_SEPARATOR = ' | ';
38
+ /** What separates the items of a list value inside one record value. */ const LIST_SEPARATOR = /\s*;\s*/;
39
+ /** The longest one record's argument may be. */ const DATASET_AI_RECORD_MAX = 600;
40
+ const TYPES = Object.keys(DATASET_DRAFT_FIELD_TYPES);
41
+ /** `"Price:number"` → `{ name: 'Price', type: 'number' }`; a type it does not know reads as text. */ export function datasetAiFieldOf(spec) {
42
+ const at = spec.lastIndexOf(':');
43
+ const type = at > 0 ? spec.slice(at + 1).trim().toLowerCase() : '';
44
+ if (at > 0 && TYPES.includes(type)) {
45
+ return {
46
+ name: spec.slice(0, at).trim(),
47
+ type: type
48
+ };
49
+ }
50
+ return {
51
+ name: spec.trim(),
52
+ type: 'text'
53
+ };
54
+ }
55
+ /** The writer's content for an item. Pure. */ export function datasetDraftContentFromArgs(args) {
56
+ const specs = Array.isArray(args['fields']) ? args['fields'] : [];
57
+ const fields = specs.map(datasetAiFieldOf).filter((field)=>field.name);
58
+ const rows = Array.isArray(args['records']) ? args['records'] : [];
59
+ const records = rows.map((row)=>{
60
+ const values = row.split(DATASET_AI_VALUE_SEPARATOR.trim()).map((value)=>value.trim());
61
+ const record = {};
62
+ fields.forEach((field, index)=>{
63
+ var _values_index;
64
+ const value = (_values_index = values[index]) != null ? _values_index : '';
65
+ if (!value) return;
66
+ record[field.name] = field.type === 'list' ? value.split(LIST_SEPARATOR).filter(Boolean) : value;
67
+ });
68
+ return record;
69
+ });
70
+ const address = typeof args['pageAddressFrom'] === 'string' ? args['pageAddressFrom'].trim() : '';
71
+ return _extends({}, typeof args['name'] === 'string' ? {
72
+ name: args['name']
73
+ } : {}, {
74
+ fields,
75
+ records: records.filter((record)=>Object.keys(record).length)
76
+ }, address ? {
77
+ pageAddressFrom: address
78
+ } : {});
79
+ }
80
+ export const datasetAiCapability = {
81
+ op: DATASET_AI_OP,
82
+ noun: 'dataset',
83
+ where: 'Data, as a dataset with its first records',
84
+ intents: [
85
+ 'a list of like things kept as records the owner edits in one place — a menu, a team, services, portfolio pieces, events, FAQs, locations — with typed fields and its first records from the request'
86
+ ],
87
+ argsSchema: {
88
+ type: 'object',
89
+ properties: {
90
+ name: {
91
+ type: 'string',
92
+ description: 'What the dataset is called, e.g. "Menu" or "Team".',
93
+ maxLength: DATASET_DRAFT_NAME_MAX
94
+ },
95
+ fields: {
96
+ type: 'array',
97
+ description: `Each field as Name:type, type one of ${TYPES.join('|')}; a date or time is text, written the way a visitor reads it. The first text field names each record.`,
98
+ items: {
99
+ type: 'string',
100
+ maxLength: DATASET_DRAFT_FIELD_NAME_MAX + 10
101
+ },
102
+ maxItems: DATASET_DRAFT_FIELDS_MAX
103
+ },
104
+ records: {
105
+ type: 'array',
106
+ description: `The first records, each its values in field order joined by "${DATASET_AI_VALUE_SEPARATOR}" (a list's items joined by "; "), from what the request says. ` + 'Leave a value empty rather than invent it: never a review, testimonial, rating, quote, price or person’s name the request does not give.',
107
+ items: {
108
+ type: 'string',
109
+ maxLength: DATASET_AI_RECORD_MAX
110
+ },
111
+ maxItems: DATASET_DRAFT_RECORDS_MAX
112
+ },
113
+ pageAddressFrom: {
114
+ type: 'string',
115
+ description: 'Only where a page shows each record at its own address: the text field its address is made from, e.g. "Name".',
116
+ maxLength: DATASET_DRAFT_FIELD_NAME_MAX
117
+ }
118
+ },
119
+ required: [
120
+ 'name',
121
+ 'fields'
122
+ ],
123
+ additionalProperties: false
124
+ },
125
+ maxPerPlan: 3,
126
+ // Free includes no data store (`datasetsPerOrg: 0`).
127
+ freeAllowed: false,
128
+ feature: 'dataStore',
129
+ quota: 'datasetsPerOrg',
130
+ draftResource: DATASET_DRAFT_RESOURCE,
131
+ // Written without a model: the planner already filled the arguments.
132
+ estimateCredits: ()=>0,
133
+ degrade: 'omit',
134
+ draftContent: (item)=>datasetDraftContentFromArgs(item.args)
135
+ };
136
+ /**
137
+ * Registers the capability; the console surface calls it beside the writer,
138
+ * since only the console runs AI jobs (AGL-3026). Idempotent.
139
+ */ export function registerDatasetAiCapability() {
140
+ registerPluginAiCapability(datasetAiCapability, {
141
+ pluginId: BUNDLE_ID
142
+ });
143
+ }
144
+
145
+ //# sourceMappingURL=dataset-ai-capability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/dataset-ai-capability.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 registerPluginAiCapability,\n type PluginAiCapability,\n type PluginAiCapabilityArgs,\n} from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n DATASET_DRAFT_FIELD_NAME_MAX,\n DATASET_DRAFT_FIELD_TYPES,\n DATASET_DRAFT_FIELDS_MAX,\n DATASET_DRAFT_NAME_MAX,\n DATASET_DRAFT_RECORDS_MAX,\n DATASET_DRAFT_RESOURCE,\n type DatasetDraftFieldType,\n} from './dataset-drafts'\n\n/**\n * WHAT AN AI BUILD CAN MAKE IN DATA (AGL-3616): a dataset with typed fields\n * and its first records.\n *\n * The capability the AI plugin's build planner offers for \"add a menu page\"\n * or \"list our team\": structured content kept as records rather than written\n * into a page, so the owner edits one row and every page listing it follows.\n * Its item is written by this plugin's `dataset` draft writer, so every rule\n * is the writer's: `dataStore` (Starter and up), `datasetsPerOrg`,\n * `release_data_store`, the member's role, and the model every record is\n * held to.\n *\n * The arguments are flat, as the contract requires: each field as\n * `Name:type`, and each record as its values in field order joined by ` | `,\n * which `draftContent` turns into the writer's records. The planner fills\n * them in, so no model runs (`estimateCredits` 0).\n */\n\n/** The operation's name in a build plan. */\nexport const DATASET_AI_OP = 'dataset'\n\n/** What separates one value of a record from the next in its argument. */\nexport const DATASET_AI_VALUE_SEPARATOR = ' | '\n\n/** What separates the items of a list value inside one record value. */\nconst LIST_SEPARATOR = /\\s*;\\s*/\n\n/** The longest one record's argument may be. */\nconst DATASET_AI_RECORD_MAX = 600\n\nconst TYPES = Object.keys(DATASET_DRAFT_FIELD_TYPES) as DatasetDraftFieldType[]\n\n/** `\"Price:number\"` → `{ name: 'Price', type: 'number' }`; a type it does not know reads as text. */\nexport function datasetAiFieldOf(spec: string): { name: string; type: DatasetDraftFieldType } {\n const at = spec.lastIndexOf(':')\n const type = at > 0 ? spec.slice(at + 1).trim().toLowerCase() : ''\n if (at > 0 && (TYPES as readonly string[]).includes(type)) {\n return { name: spec.slice(0, at).trim(), type: type as DatasetDraftFieldType }\n }\n return { name: spec.trim(), type: 'text' }\n}\n\n/** The writer's content for an item. Pure. */\nexport function datasetDraftContentFromArgs(args: PluginAiCapabilityArgs): Readonly<Record<string, unknown>> {\n const specs = Array.isArray(args['fields']) ? (args['fields'] as readonly string[]) : []\n const fields = specs.map(datasetAiFieldOf).filter((field) => field.name)\n const rows = Array.isArray(args['records']) ? (args['records'] as readonly string[]) : []\n const records = rows.map((row) => {\n const values = row.split(DATASET_AI_VALUE_SEPARATOR.trim()).map((value) => value.trim())\n const record: Record<string, unknown> = {}\n fields.forEach((field, index) => {\n const value = values[index] ?? ''\n if (!value) return\n record[field.name] = field.type === 'list' ? value.split(LIST_SEPARATOR).filter(Boolean) : value\n })\n return record\n })\n const address = typeof args['pageAddressFrom'] === 'string' ? args['pageAddressFrom'].trim() : ''\n return {\n ...(typeof args['name'] === 'string' ? { name: args['name'] } : {}),\n fields,\n records: records.filter((record) => Object.keys(record).length),\n ...(address ? { pageAddressFrom: address } : {}),\n }\n}\n\nexport const datasetAiCapability: PluginAiCapability = {\n op: DATASET_AI_OP,\n noun: 'dataset',\n where: 'Data, as a dataset with its first records',\n intents: [\n 'a list of like things kept as records the owner edits in one place — a menu, a team, services, portfolio pieces, events, FAQs, locations — with typed fields and its first records from the request',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'What the dataset is called, e.g. \"Menu\" or \"Team\".', maxLength: DATASET_DRAFT_NAME_MAX },\n fields: {\n type: 'array',\n description: `Each field as Name:type, type one of ${TYPES.join('|')}; a date or time is text, written the way a visitor reads it. The first text field names each record.`,\n items: { type: 'string', maxLength: DATASET_DRAFT_FIELD_NAME_MAX + 10 },\n maxItems: DATASET_DRAFT_FIELDS_MAX,\n },\n records: {\n type: 'array',\n description:\n `The first records, each its values in field order joined by \"${DATASET_AI_VALUE_SEPARATOR}\" (a list's items joined by \"; \"), from what the request says. ` +\n 'Leave a value empty rather than invent it: never a review, testimonial, rating, quote, price or person’s name the request does not give.',\n items: { type: 'string', maxLength: DATASET_AI_RECORD_MAX },\n maxItems: DATASET_DRAFT_RECORDS_MAX,\n },\n pageAddressFrom: {\n type: 'string',\n description: 'Only where a page shows each record at its own address: the text field its address is made from, e.g. \"Name\".',\n maxLength: DATASET_DRAFT_FIELD_NAME_MAX,\n },\n },\n required: ['name', 'fields'],\n additionalProperties: false,\n },\n maxPerPlan: 3,\n // Free includes no data store (`datasetsPerOrg: 0`).\n freeAllowed: false,\n feature: 'dataStore',\n quota: 'datasetsPerOrg',\n draftResource: DATASET_DRAFT_RESOURCE,\n // Written without a model: the planner already filled the arguments.\n estimateCredits: () => 0,\n degrade: 'omit',\n draftContent: (item) => datasetDraftContentFromArgs(item.args),\n}\n\n/**\n * Registers the capability; the console surface calls it beside the writer,\n * since only the console runs AI jobs (AGL-3026). Idempotent.\n */\nexport function registerDatasetAiCapability(): void {\n registerPluginAiCapability(datasetAiCapability, { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginAiCapability","BUNDLE_ID","DATASET_DRAFT_FIELD_NAME_MAX","DATASET_DRAFT_FIELD_TYPES","DATASET_DRAFT_FIELDS_MAX","DATASET_DRAFT_NAME_MAX","DATASET_DRAFT_RECORDS_MAX","DATASET_DRAFT_RESOURCE","DATASET_AI_OP","DATASET_AI_VALUE_SEPARATOR","LIST_SEPARATOR","DATASET_AI_RECORD_MAX","TYPES","Object","keys","datasetAiFieldOf","spec","at","lastIndexOf","type","slice","trim","toLowerCase","includes","name","datasetDraftContentFromArgs","args","specs","Array","isArray","fields","map","filter","field","rows","records","row","values","split","value","record","forEach","index","Boolean","address","length","pageAddressFrom","datasetAiCapability","op","noun","where","intents","argsSchema","properties","description","maxLength","join","items","maxItems","required","additionalProperties","maxPerPlan","freeAllowed","feature","quota","draftResource","estimateCredits","degrade","draftContent","item","registerDatasetAiCapability","pluginId"],"mappings":"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"}
@@ -0,0 +1,169 @@
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 { checkDatasetQuota } from '@aglyn/aglyn/app-utils/plan-entitlements';
18
+ import { type PluginDraftCheck, type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
19
+ import { type DatasetFieldType, type DatasetModel } from '../model/dataset-models';
20
+ /**
21
+ * A DATASET ANOTHER PLUGIN ASKS FOR (AGL-3616).
22
+ *
23
+ * The draft writer this plugin registers for the `dataset` resource on the
24
+ * core's resource-drafts seam. Aglyn AI builds a menu, a team, a list of
25
+ * services or a portfolio as a dataset, the way the docs teach a person to,
26
+ * and asks for the writer by name, so every rule is this plugin's:
27
+ *
28
+ * - THE DOCUMENT is the one the Data card's create makes: a display name,
29
+ * the flat `fields` mirror and a typed model, the org's Default sharing
30
+ * for the site it is made from (`defaultScopeForNewResource`), and the
31
+ * seeded records, each with the integrity and filter index every record
32
+ * write carries. Where a record page is wanted the model gains a "Page
33
+ * address" field that fills from a text field, and every seeded record a
34
+ * unique address (`uniqueRecordAddresses`), so the member's Record pages
35
+ * save has nothing left to fill.
36
+ * - THE ROOM is `datasetsPerOrg` (add-ons included, `checkDatasetQuota`),
37
+ * counted INSIDE the transaction that creates, as the datasets route
38
+ * counts it; the records are held to `recordsPerDataset` and the dataset
39
+ * storage band.
40
+ * - THE PLAN is `dataStore` (Starter and up), THE FLAG `release_data_store`,
41
+ * and THE ROLE an org member who may edit org data (owner, admin or
42
+ * editor) with `data.manage`, whose access reaches the site — refused in
43
+ * the route's own words.
44
+ * - THE CHECK holds every record to the model it is written under
45
+ * (`validateDocument`), so what is seeded is what a person could type.
46
+ *
47
+ * Nothing is published. The writer touches the one new dataset and its
48
+ * records; a page shows them only where a person (or a job's draft page)
49
+ * places a repeat over it.
50
+ */
51
+ type Firestore = FirebaseFirestore.Firestore;
52
+ /** The resource name this writer is registered under. */
53
+ export declare const DATASET_DRAFT_RESOURCE = "dataset";
54
+ /** The longest dataset name, as the datasets route keeps it. */
55
+ export declare const DATASET_DRAFT_NAME_MAX = 120;
56
+ /** The most fields a drafted dataset is made with, and the longest name. */
57
+ export declare const DATASET_DRAFT_FIELDS_MAX = 16;
58
+ export declare const DATASET_DRAFT_FIELD_NAME_MAX = 60;
59
+ /** The most records a draft is seeded with. */
60
+ export declare const DATASET_DRAFT_RECORDS_MAX = 24;
61
+ /** The longest text value a seeded record keeps. */
62
+ export declare const DATASET_DRAFT_VALUE_MAX = 2000;
63
+ /** The most entries a seeded list value keeps. */
64
+ export declare const DATASET_DRAFT_LIST_MAX = 12;
65
+ /**
66
+ * The field types a caller names, in words a person reads, and the type
67
+ * each one is stored as. A date or a time is written as text the way a
68
+ * visitor reads it ("Saturday 8 November, 7 pm"): a stored timestamp renders
69
+ * as a number in a repeat's `{{item.<field>}}`.
70
+ */
71
+ export declare const DATASET_DRAFT_FIELD_TYPES: {
72
+ readonly text: "text";
73
+ readonly number: "float";
74
+ readonly integer: "int32";
75
+ readonly boolean: "bool";
76
+ readonly list: "sorted";
77
+ };
78
+ export type DatasetDraftFieldType = keyof typeof DATASET_DRAFT_FIELD_TYPES;
79
+ /** The datasets route's refusals, word for word. */
80
+ export declare const DATASET_DRAFT_ROLE_REFUSAL = "Editing org data requires the editor role";
81
+ export declare const DATASET_DRAFT_PERMISSION_REFUSAL = "Your organization role does not allow editing organization data";
82
+ export declare const DATASET_DRAFT_PLAN_REFUSAL = "Datasets require a Starter plan or higher";
83
+ export declare const DATASET_DRAFT_RELEASE_REFUSAL = "Datasets are not available for this workspace yet";
84
+ export declare const DATASET_DRAFT_SCOPE_REFUSAL = "Your access to this organization does not reach that site";
85
+ export declare const DATASET_DRAFT_STORAGE_REFUSAL = "Dataset storage is full on this plan \u2014 upgrade in Billing";
86
+ /** The route's refusal at the plan's dataset allowance. */
87
+ export declare function datasetDraftLimitRefusal(quota: ReturnType<typeof checkDatasetQuota>): string;
88
+ /** The route's refusal at the plan's record allowance. */
89
+ export declare function datasetDraftRecordLimitRefusal(limit: number): string;
90
+ /** One field as a caller names it. */
91
+ export interface DatasetDraftField {
92
+ name: string;
93
+ /** Absent is text. */
94
+ type?: DatasetDraftFieldType;
95
+ required?: boolean;
96
+ }
97
+ /**
98
+ * What a caller sends as `content`. Each record's values are keyed by the
99
+ * field NAME the caller gave, since the caller has no ids yet; the writer
100
+ * mints the ids (`roast_preference` from "Roast preference").
101
+ */
102
+ export interface DatasetDraftContent {
103
+ /** The dataset's name; the request's `name` stands in when absent. */
104
+ name?: string;
105
+ fields: DatasetDraftField[];
106
+ records?: Array<Record<string, unknown>>;
107
+ /**
108
+ * A text field, by name, a "Page address" field fills from (AGL-3475):
109
+ * asked for where a record page will show each record at its own address.
110
+ */
111
+ pageAddressFrom?: string;
112
+ }
113
+ /** What the writer reports about a dataset. */
114
+ export interface DatasetDraftFacts {
115
+ fields: Array<{
116
+ id: string;
117
+ name: string;
118
+ type: DatasetFieldType;
119
+ }>;
120
+ records: number;
121
+ /** The page-address field's id, where the dataset has one. */
122
+ addressField: string | null;
123
+ }
124
+ /** The draft as the writer will store it. */
125
+ export interface DatasetDraftRead {
126
+ name: string;
127
+ model: DatasetModel;
128
+ /** Each record's values by field id, coerced and addressed. */
129
+ records: Array<Record<string, unknown>>;
130
+ addressField: string | null;
131
+ }
132
+ export type DatasetDraftContentRead = {
133
+ ok: true;
134
+ value: DatasetDraftRead;
135
+ } | {
136
+ ok: false;
137
+ problems: string[];
138
+ };
139
+ /** The content as the writer reads it, or every problem that stops it. Pure. */
140
+ export declare function readDatasetDraftContent(content: Readonly<Record<string, unknown>>): DatasetDraftContentRead;
141
+ /** Whether content is a dataset this plugin would store, with what it says about it. Pure. */
142
+ export declare function checkDatasetDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck;
143
+ /** Who the member is in the org, as the writer asks: their role, and whether they may manage data. */
144
+ export interface DatasetDraftMember {
145
+ role: string;
146
+ canManageData: boolean;
147
+ /** The membership itself, for whether their access reaches a site. */
148
+ member: Readonly<Record<string, unknown>>;
149
+ }
150
+ export interface DatasetDraftWriterDeps {
151
+ /** The Admin SDK handle; specs hand in a double. */
152
+ firestore?: () => Firestore;
153
+ /** The org a site belongs to; the platform's lookup otherwise. */
154
+ orgIdForHost?: (hostId: string) => Promise<string | null>;
155
+ /** The member, or `null` when they are not one; the platform's otherwise. */
156
+ member?: (uid: string, orgId: string) => Promise<DatasetDraftMember | null>;
157
+ /** Whether the data store is released for the org; the platform's flag otherwise. */
158
+ released?: (orgId: string) => Promise<boolean>;
159
+ /** Whether dataset bytes may grow; the platform's band otherwise. */
160
+ storageFull?: (org: unknown, orgRef: FirebaseFirestore.DocumentReference) => Promise<boolean>;
161
+ }
162
+ export declare function createDatasetDraftWriter(deps?: DatasetDraftWriterDeps): PluginResourceDraftWriter;
163
+ export declare const datasetDraftWriter: PluginResourceDraftWriter;
164
+ /**
165
+ * Registers the writer; the console surface calls it, since only the console
166
+ * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.
167
+ */
168
+ export declare function registerDatasetDraftWriter(): void;
169
+ export {};
@@ -0,0 +1,426 @@
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 { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
18
+ import { memberCanSee } from "@aglyn/aglyn/app-utils/organizations";
19
+ import { checkDatasetQuota, checkEntitlement, checkQuota } from "@aglyn/aglyn/app-utils/plan-entitlements";
20
+ import { defaultScopeForNewResource, newResourceScopeFields } from "@aglyn/aglyn/app-utils/scope-tokens";
21
+ import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
22
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
23
+ import { dataStorageRefusal } from "@aglyn/tenant-data-admin/server/data-storage-gate";
24
+ import { memberHasOrgPermission, resolveOrgIdForHost, resolveOrgMembership } from "@aglyn/tenant-data-admin/server/organizations";
25
+ import { isServerReleaseFlagOnForOrg } from "@aglyn/tenant-data-admin/server/release-flags";
26
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
27
+ import { coerceDocumentValues, datasetIntegrityFields, defaultDatasetFieldId, validateDocument } from "../model/dataset-models.js";
28
+ import { RECORD_PAGE_ADDRESS_FIELD_TYPE, RECORD_PAGE_ADDRESS_MAX, uniqueRecordAddresses } from "../record-pages/record-pages.js";
29
+ /** The resource name this writer is registered under. */ export const DATASET_DRAFT_RESOURCE = 'dataset';
30
+ /** The longest dataset name, as the datasets route keeps it. */ export const DATASET_DRAFT_NAME_MAX = 120;
31
+ /** The most fields a drafted dataset is made with, and the longest name. */ export const DATASET_DRAFT_FIELDS_MAX = 16;
32
+ export const DATASET_DRAFT_FIELD_NAME_MAX = 60;
33
+ /** The most records a draft is seeded with. */ export const DATASET_DRAFT_RECORDS_MAX = 24;
34
+ /** The longest text value a seeded record keeps. */ export const DATASET_DRAFT_VALUE_MAX = 2000;
35
+ /** The most entries a seeded list value keeps. */ export const DATASET_DRAFT_LIST_MAX = 12;
36
+ /**
37
+ * The field types a caller names, in words a person reads, and the type
38
+ * each one is stored as. A date or a time is written as text the way a
39
+ * visitor reads it ("Saturday 8 November, 7 pm"): a stored timestamp renders
40
+ * as a number in a repeat's `{{item.<field>}}`.
41
+ */ export const DATASET_DRAFT_FIELD_TYPES = {
42
+ text: 'text',
43
+ number: 'float',
44
+ integer: 'int32',
45
+ boolean: 'bool',
46
+ list: 'sorted'
47
+ };
48
+ const DRAFT_TYPES = Object.keys(DATASET_DRAFT_FIELD_TYPES);
49
+ /** The datasets route's refusals, word for word. */ export const DATASET_DRAFT_ROLE_REFUSAL = 'Editing org data requires the editor role';
50
+ export const DATASET_DRAFT_PERMISSION_REFUSAL = 'Your organization role does not allow editing organization data';
51
+ export const DATASET_DRAFT_PLAN_REFUSAL = 'Datasets require a Starter plan or higher';
52
+ export const DATASET_DRAFT_RELEASE_REFUSAL = 'Datasets are not available for this workspace yet';
53
+ export const DATASET_DRAFT_SCOPE_REFUSAL = 'Your access to this organization does not reach that site';
54
+ export const DATASET_DRAFT_STORAGE_REFUSAL = 'Dataset storage is full on this plan — upgrade in Billing';
55
+ /** The route's refusal at the plan's dataset allowance. */ export function datasetDraftLimitRefusal(quota) {
56
+ return quota.upgradeRequired ? `Dataset limit reached (${quota.limit}) — upgrade in Billing` : `Dataset limit reached (${quota.limit}) — add extra datasets for $${quota.addonPriceUsd}/mo each or upgrade in Billing`;
57
+ }
58
+ /** The route's refusal at the plan's record allowance. */ export function datasetDraftRecordLimitRefusal(limit) {
59
+ return `Record limit reached (${limit}) — upgrade in Billing`;
60
+ }
61
+ function isRecord(value) {
62
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
63
+ }
64
+ /** Text on one line: controls and runs of space folded to one space. */ function line(value) {
65
+ return typeof value === 'string' ? value.replace(RegExp("[\\u0000-\\u001F\\u007F-\\u009F\\s]+", "gu"), ' ').trim() : '';
66
+ }
67
+ /** A value as a seeded record keeps it, before the model coerces it. */ function seedValue(value) {
68
+ if (typeof value === 'string') return value.replace(/\r\n?/g, '\n').trim().slice(0, DATASET_DRAFT_VALUE_MAX);
69
+ if (Array.isArray(value)) {
70
+ return value.map((entry)=>typeof entry === 'string' ? line(entry) : entry).filter((entry)=>entry !== '' && entry !== null && entry !== undefined).slice(0, DATASET_DRAFT_LIST_MAX);
71
+ }
72
+ return value;
73
+ }
74
+ /** The content as the writer reads it, or every problem that stops it. Pure. */ export function readDatasetDraftContent(content) {
75
+ var _content_records;
76
+ const problems = [];
77
+ const name = line(content['name']);
78
+ if (!name) problems.push('The dataset needs a name');
79
+ else if (name.length > DATASET_DRAFT_NAME_MAX) {
80
+ problems.push(`The name is longer than ${DATASET_DRAFT_NAME_MAX} characters`);
81
+ }
82
+ const rawFields = content['fields'];
83
+ const model = {
84
+ fields: {},
85
+ order: []
86
+ };
87
+ /** Field name, lowercased → id. */ const byName = new Map();
88
+ if (!Array.isArray(rawFields) || !rawFields.length) {
89
+ problems.push('The dataset needs at least one field');
90
+ } else if (rawFields.length > DATASET_DRAFT_FIELDS_MAX) {
91
+ problems.push(`A dataset is made with at most ${DATASET_DRAFT_FIELDS_MAX} fields`);
92
+ } else {
93
+ for (const raw of rawFields){
94
+ var _ref;
95
+ const fieldName = isRecord(raw) ? line(raw['name']) : '';
96
+ const rawType = (_ref = isRecord(raw) ? raw['type'] : undefined) != null ? _ref : 'text';
97
+ if (!fieldName) {
98
+ problems.push('Each field needs a name');
99
+ continue;
100
+ }
101
+ if (fieldName.length > DATASET_DRAFT_FIELD_NAME_MAX) {
102
+ problems.push(`The field "${fieldName.slice(0, 20)}…" has a name longer than ${DATASET_DRAFT_FIELD_NAME_MAX} characters`);
103
+ continue;
104
+ }
105
+ if (!DRAFT_TYPES.includes(rawType)) {
106
+ problems.push(`The field "${fieldName}" is one of ${DRAFT_TYPES.join(', ')}`);
107
+ continue;
108
+ }
109
+ if (byName.has(fieldName.toLowerCase())) {
110
+ problems.push(`Two fields are both called "${fieldName}"`);
111
+ continue;
112
+ }
113
+ const id = defaultDatasetFieldId(fieldName, new Set(model.order));
114
+ if (!id) {
115
+ problems.push(`The field "${fieldName}" needs a name with a letter in it`);
116
+ continue;
117
+ }
118
+ model.fields[id] = _extends({
119
+ name: fieldName,
120
+ type: DATASET_DRAFT_FIELD_TYPES[rawType]
121
+ }, isRecord(raw) && raw['required'] === true ? {
122
+ required: true
123
+ } : {});
124
+ model.order.push(id);
125
+ byName.set(fieldName.toLowerCase(), id);
126
+ }
127
+ }
128
+ // The page address: a text field the address fills from, once.
129
+ let addressField = null;
130
+ const rawAddress = content['pageAddressFrom'];
131
+ if (rawAddress !== undefined && rawAddress !== null && rawAddress !== '') {
132
+ var _model_fields_sourceId;
133
+ const sourceId = byName.get(line(rawAddress).toLowerCase());
134
+ if (!sourceId || ((_model_fields_sourceId = model.fields[sourceId]) == null ? void 0 : _model_fields_sourceId.type) !== 'text') {
135
+ problems.push('A page address fills from one of the dataset’s text fields');
136
+ } else {
137
+ addressField = defaultDatasetFieldId('slug', new Set(model.order));
138
+ model.fields[addressField] = {
139
+ name: 'Page address',
140
+ type: 'text',
141
+ customType: RECORD_PAGE_ADDRESS_FIELD_TYPE,
142
+ slugFrom: sourceId
143
+ };
144
+ model.order.push(addressField);
145
+ }
146
+ }
147
+ const rawRecords = (_content_records = content['records']) != null ? _content_records : [];
148
+ const records = [];
149
+ if (!Array.isArray(rawRecords)) {
150
+ problems.push('Records are a list of values by field name');
151
+ } else if (rawRecords.length > DATASET_DRAFT_RECORDS_MAX) {
152
+ problems.push(`A dataset is seeded with at most ${DATASET_DRAFT_RECORDS_MAX} records`);
153
+ } else if (!problems.length) {
154
+ rawRecords.forEach((raw, index)=>{
155
+ if (!isRecord(raw)) {
156
+ problems.push(`Record ${index + 1} is not a set of values`);
157
+ return;
158
+ }
159
+ const input = {};
160
+ for (const [key, value] of Object.entries(raw)){
161
+ const id = byName.get(line(key).toLowerCase());
162
+ if (!id) {
163
+ problems.push(`Record ${index + 1} names "${line(key).slice(0, 40)}", which is not a field`);
164
+ continue;
165
+ }
166
+ input[id] = seedValue(value);
167
+ }
168
+ const values = coerceDocumentValues(model, input);
169
+ // An empty value is no value: it is not stored, as the card stores none.
170
+ for (const [id, value] of Object.entries(values)){
171
+ if (value === '' || value === null || value === undefined || Array.isArray(value) && !value.length) {
172
+ delete values[id];
173
+ }
174
+ }
175
+ const errors = validateDocument(model, values);
176
+ for (const error of Object.values(errors))problems.push(`Record ${index + 1}: ${error}`);
177
+ if (!Object.keys(values).length) problems.push(`Record ${index + 1} has no values`);
178
+ records.push(values);
179
+ });
180
+ }
181
+ if (problems.length) return {
182
+ ok: false,
183
+ problems: [
184
+ ...new Set(problems)
185
+ ]
186
+ };
187
+ // Every seeded record its own address, unique within the dataset.
188
+ if (addressField) {
189
+ var _model_fields_addressField;
190
+ const source = (_model_fields_addressField = model.fields[addressField]) == null ? void 0 : _model_fields_addressField.slugFrom;
191
+ const assigned = uniqueRecordAddresses(records.map((values, index)=>({
192
+ id: String(index),
193
+ source: values[source]
194
+ })));
195
+ records.forEach((values, index)=>{
196
+ const address = assigned.get(String(index));
197
+ if (address) values[addressField] = address.slice(0, RECORD_PAGE_ADDRESS_MAX);
198
+ });
199
+ }
200
+ return {
201
+ ok: true,
202
+ value: {
203
+ name,
204
+ model,
205
+ records,
206
+ addressField
207
+ }
208
+ };
209
+ }
210
+ function factsOf(model, records, addressField) {
211
+ return {
212
+ fields: model.order.map((id)=>{
213
+ var _ref, _ref1;
214
+ var _model_fields_id, _model_fields_id1;
215
+ return {
216
+ id,
217
+ name: (_ref = (_model_fields_id = model.fields[id]) == null ? void 0 : _model_fields_id.name) != null ? _ref : id,
218
+ type: (_ref1 = (_model_fields_id1 = model.fields[id]) == null ? void 0 : _model_fields_id1.type) != null ? _ref1 : 'text'
219
+ };
220
+ }),
221
+ records,
222
+ addressField
223
+ };
224
+ }
225
+ /** Whether content is a dataset this plugin would store, with what it says about it. Pure. */ export function checkDatasetDraftContent(content) {
226
+ const read = readDatasetDraftContent(content);
227
+ if (read.ok === false) return read;
228
+ return {
229
+ ok: true,
230
+ facts: factsOf(read.value.model, read.value.records.length, read.value.addressField)
231
+ };
232
+ }
233
+ /** Roles allowed to create org data — the datasets route's, mirroring the rules' `canWriteOrgData()`. */ const WRITER_ROLES = new Set([
234
+ 'owner',
235
+ 'admin',
236
+ 'editor'
237
+ ]);
238
+ async function platformMember(uid, orgId) {
239
+ var _member_role;
240
+ const membership = await resolveOrgMembership(uid, orgId);
241
+ const member = membership == null ? void 0 : membership.member;
242
+ if (!member) return null;
243
+ return {
244
+ role: String((_member_role = member.role) != null ? _member_role : ''),
245
+ canManageData: await memberHasOrgPermission(orgId, member, 'data.manage'),
246
+ member: member
247
+ };
248
+ }
249
+ export function createDatasetDraftWriter(deps = {}) {
250
+ var _deps_firestore, _deps_orgIdForHost, _deps_member, _deps_released, _deps_storageFull;
251
+ const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : ()=>firebaseAdmin.app().firestore();
252
+ const orgIdForHost = (_deps_orgIdForHost = deps.orgIdForHost) != null ? _deps_orgIdForHost : (hostId)=>resolveOrgIdForHost(hostId);
253
+ const memberOf = (_deps_member = deps.member) != null ? _deps_member : platformMember;
254
+ const released = (_deps_released = deps.released) != null ? _deps_released : (orgId)=>isServerReleaseFlagOnForOrg('release_data_store', orgId);
255
+ const storageFull = (_deps_storageFull = deps.storageFull) != null ? _deps_storageFull : async (org, orgRef)=>Boolean(await dataStorageRefusal(org, orgRef));
256
+ /** Everything but the room: the site is the org's, the member's role, the flag, the plan, the scope. */ const early = async (context)=>{
257
+ if (await orgIdForHost(context.hostId) !== context.orgId) return {
258
+ status: 404,
259
+ error: 'Unknown site'
260
+ };
261
+ const member = await memberOf(context.uid, context.orgId);
262
+ if (!member || !WRITER_ROLES.has(member.role)) return {
263
+ status: 403,
264
+ error: DATASET_DRAFT_ROLE_REFUSAL
265
+ };
266
+ if (!member.canManageData) return {
267
+ status: 403,
268
+ error: DATASET_DRAFT_PERMISSION_REFUSAL
269
+ };
270
+ if (!await released(context.orgId)) return {
271
+ status: 403,
272
+ error: DATASET_DRAFT_RELEASE_REFUSAL
273
+ };
274
+ if (!checkEntitlement(context.org, 'dataStore')) return {
275
+ status: 403,
276
+ error: DATASET_DRAFT_PLAN_REFUSAL
277
+ };
278
+ // A member scoped to some sites must be able to see what they create.
279
+ if (!memberCanSee(member.member, scopeOf(context))) {
280
+ return {
281
+ status: 403,
282
+ error: DATASET_DRAFT_SCOPE_REFUSAL
283
+ };
284
+ }
285
+ return null;
286
+ };
287
+ return {
288
+ refusal: async (context)=>{
289
+ const refused = await early(context);
290
+ if (refused) return refused;
291
+ const used = (await firestore().collection('orgs').doc(context.orgId).collection('datasets').count().get()).data().count;
292
+ const quota = checkDatasetQuota(context.org, Number(used) || 0);
293
+ return quota.allowed ? null : {
294
+ status: 403,
295
+ error: datasetDraftLimitRefusal(quota)
296
+ };
297
+ },
298
+ check: (content)=>checkDatasetDraftContent(content),
299
+ read: async ({ hostId, id })=>{
300
+ const orgId = await orgIdForHost(hostId);
301
+ if (!orgId) return null;
302
+ const dataset = await firestore().collection('orgs').doc(orgId).collection('datasets').doc(id).get();
303
+ return dataset.exists ? recordOf(dataset) : null;
304
+ },
305
+ write: async (request)=>{
306
+ // The request's name is the one asked for; the content's stands in.
307
+ const asked = line(request.name);
308
+ const read = readDatasetDraftContent(asked ? _extends({}, request.content, {
309
+ name: asked
310
+ }) : request.content);
311
+ if (read.ok === false) return {
312
+ ok: false,
313
+ status: 400,
314
+ error: read.problems[0]
315
+ };
316
+ const refused = await early(request);
317
+ if (refused) return _extends({
318
+ ok: false
319
+ }, refused);
320
+ const db = firestore();
321
+ const orgRef = db.collection('orgs').doc(request.orgId);
322
+ const datasets = orgRef.collection('datasets');
323
+ const datasetRef = datasets.doc(request.id);
324
+ const { name, model, records, addressField } = read.value;
325
+ const recordQuota = checkQuota(request.org, 'recordsPerDataset', Math.max(0, records.length - 1));
326
+ if (records.length && !recordQuota.allowed) {
327
+ return {
328
+ ok: false,
329
+ status: 403,
330
+ error: datasetDraftRecordLimitRefusal(recordQuota.limit)
331
+ };
332
+ }
333
+ if (records.length && await storageFull(request.org, orgRef)) {
334
+ return {
335
+ ok: false,
336
+ status: 403,
337
+ error: DATASET_DRAFT_STORAGE_REFUSAL
338
+ };
339
+ }
340
+ const visibleTo = scopeOf(request);
341
+ // Minted before the transaction, so a retried attempt writes the same rows.
342
+ const recordIds = records.map(()=>createResourceUid());
343
+ return db.runTransaction(async (tx)=>{
344
+ // Every read before any write, which Firestore requires.
345
+ const existing = await tx.get(datasetRef);
346
+ // Asked again under the same id: the dataset it already wrote.
347
+ if (existing.exists) return _extends({
348
+ ok: true,
349
+ replayed: true
350
+ }, recordOf(existing));
351
+ const live = (await tx.get(datasets.count())).data().count;
352
+ const quota = checkDatasetQuota(request.org, Number(live) || 0);
353
+ if (!quota.allowed) return {
354
+ ok: false,
355
+ status: 403,
356
+ error: datasetDraftLimitRefusal(quota)
357
+ };
358
+ tx.create(datasetRef, _extends({
359
+ displayName: name,
360
+ fields: [
361
+ ...model.order
362
+ ],
363
+ model
364
+ }, newResourceScopeFields(visibleTo), {
365
+ createdAt: request.now,
366
+ createdBy: request.uid
367
+ }));
368
+ records.forEach((values, order)=>{
369
+ tx.create(datasetRef.collection('records').doc(recordIds[order]), _extends({
370
+ values
371
+ }, datasetIntegrityFields(model, values), {
372
+ order,
373
+ createdAt: request.now,
374
+ updatedAt: request.now
375
+ }));
376
+ });
377
+ return {
378
+ ok: true,
379
+ replayed: false,
380
+ id: request.id,
381
+ name,
382
+ versionId: null,
383
+ facts: factsOf(model, records.length, addressField)
384
+ };
385
+ });
386
+ }
387
+ };
388
+ }
389
+ /** The scope a dataset made from this site takes: the org's Default sharing for it. */ function scopeOf(context) {
390
+ var _context_org;
391
+ return defaultScopeForNewResource({
392
+ defaultResourceScope: (_context_org = context.org) == null ? void 0 : _context_org.defaultResourceScope,
393
+ hostId: context.hostId
394
+ });
395
+ }
396
+ function recordOf(dataset) {
397
+ var _dataset_data, _model_order_find, _data_displayName;
398
+ var _data_model;
399
+ const data = (_dataset_data = dataset.data()) != null ? _dataset_data : {};
400
+ const model = ((_data_model = data.model) == null ? void 0 : _data_model.order) ? data.model : {
401
+ fields: {},
402
+ order: []
403
+ };
404
+ const addressField = (_model_order_find = model.order.find((id)=>{
405
+ var _model_fields_id;
406
+ return ((_model_fields_id = model.fields[id]) == null ? void 0 : _model_fields_id.customType) === RECORD_PAGE_ADDRESS_FIELD_TYPE;
407
+ })) != null ? _model_order_find : null;
408
+ return {
409
+ id: dataset.id,
410
+ name: String((_data_displayName = data.displayName) != null ? _data_displayName : ''),
411
+ versionId: null,
412
+ // A replay reports the fields; how many records it was seeded with is the first write's to say.
413
+ facts: factsOf(model, 0, addressField)
414
+ };
415
+ }
416
+ export const datasetDraftWriter = createDatasetDraftWriter();
417
+ /**
418
+ * Registers the writer; the console surface calls it, since only the console
419
+ * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.
420
+ */ export function registerDatasetDraftWriter() {
421
+ registerPluginResourceDraftWriter(DATASET_DRAFT_RESOURCE, datasetDraftWriter, {
422
+ pluginId: BUNDLE_ID
423
+ });
424
+ }
425
+
426
+ //# sourceMappingURL=dataset-drafts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/dataset-drafts.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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { memberCanSee } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkDatasetQuota, checkEntitlement, checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { defaultScopeForNewResource, newResourceScopeFields } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftContext,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { dataStorageRefusal } from '@aglyn/tenant-data-admin/server/data-storage-gate'\nimport {\n memberHasOrgPermission,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport { isServerReleaseFlagOnForOrg } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n coerceDocumentValues,\n datasetIntegrityFields,\n defaultDatasetFieldId,\n validateDocument,\n type DatasetFieldType,\n type DatasetModel,\n} from '../model/dataset-models'\nimport {\n RECORD_PAGE_ADDRESS_FIELD_TYPE,\n RECORD_PAGE_ADDRESS_MAX,\n uniqueRecordAddresses,\n} from '../record-pages/record-pages'\n\n/**\n * A DATASET ANOTHER PLUGIN ASKS FOR (AGL-3616).\n *\n * The draft writer this plugin registers for the `dataset` resource on the\n * core's resource-drafts seam. Aglyn AI builds a menu, a team, a list of\n * services or a portfolio as a dataset, the way the docs teach a person to,\n * and asks for the writer by name, so every rule is this plugin's:\n *\n * - THE DOCUMENT is the one the Data card's create makes: a display name,\n * the flat `fields` mirror and a typed model, the org's Default sharing\n * for the site it is made from (`defaultScopeForNewResource`), and the\n * seeded records, each with the integrity and filter index every record\n * write carries. Where a record page is wanted the model gains a \"Page\n * address\" field that fills from a text field, and every seeded record a\n * unique address (`uniqueRecordAddresses`), so the member's Record pages\n * save has nothing left to fill.\n * - THE ROOM is `datasetsPerOrg` (add-ons included, `checkDatasetQuota`),\n * counted INSIDE the transaction that creates, as the datasets route\n * counts it; the records are held to `recordsPerDataset` and the dataset\n * storage band.\n * - THE PLAN is `dataStore` (Starter and up), THE FLAG `release_data_store`,\n * and THE ROLE an org member who may edit org data (owner, admin or\n * editor) with `data.manage`, whose access reaches the site — refused in\n * the route's own words.\n * - THE CHECK holds every record to the model it is written under\n * (`validateDocument`), so what is seeded is what a person could type.\n *\n * Nothing is published. The writer touches the one new dataset and its\n * records; a page shows them only where a person (or a job's draft page)\n * places a repeat over it.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const DATASET_DRAFT_RESOURCE = 'dataset'\n\n/** The longest dataset name, as the datasets route keeps it. */\nexport const DATASET_DRAFT_NAME_MAX = 120\n/** The most fields a drafted dataset is made with, and the longest name. */\nexport const DATASET_DRAFT_FIELDS_MAX = 16\nexport const DATASET_DRAFT_FIELD_NAME_MAX = 60\n/** The most records a draft is seeded with. */\nexport const DATASET_DRAFT_RECORDS_MAX = 24\n/** The longest text value a seeded record keeps. */\nexport const DATASET_DRAFT_VALUE_MAX = 2_000\n/** The most entries a seeded list value keeps. */\nexport const DATASET_DRAFT_LIST_MAX = 12\n\n/**\n * The field types a caller names, in words a person reads, and the type\n * each one is stored as. A date or a time is written as text the way a\n * visitor reads it (\"Saturday 8 November, 7 pm\"): a stored timestamp renders\n * as a number in a repeat's `{{item.<field>}}`.\n */\nexport const DATASET_DRAFT_FIELD_TYPES = {\n text: 'text',\n number: 'float',\n integer: 'int32',\n boolean: 'bool',\n list: 'sorted',\n} as const satisfies Record<string, DatasetFieldType>\n\nexport type DatasetDraftFieldType = keyof typeof DATASET_DRAFT_FIELD_TYPES\n\nconst DRAFT_TYPES = Object.keys(DATASET_DRAFT_FIELD_TYPES) as DatasetDraftFieldType[]\n\n/** The datasets route's refusals, word for word. */\nexport const DATASET_DRAFT_ROLE_REFUSAL = 'Editing org data requires the editor role'\nexport const DATASET_DRAFT_PERMISSION_REFUSAL = 'Your organization role does not allow editing organization data'\nexport const DATASET_DRAFT_PLAN_REFUSAL = 'Datasets require a Starter plan or higher'\nexport const DATASET_DRAFT_RELEASE_REFUSAL = 'Datasets are not available for this workspace yet'\nexport const DATASET_DRAFT_SCOPE_REFUSAL = 'Your access to this organization does not reach that site'\nexport const DATASET_DRAFT_STORAGE_REFUSAL = 'Dataset storage is full on this plan — upgrade in Billing'\n\n/** The route's refusal at the plan's dataset allowance. */\nexport function datasetDraftLimitRefusal(quota: ReturnType<typeof checkDatasetQuota>): string {\n return quota.upgradeRequired\n ? `Dataset limit reached (${quota.limit}) — upgrade in Billing`\n : `Dataset limit reached (${quota.limit}) — add extra datasets for $${quota.addonPriceUsd}/mo each or upgrade in Billing`\n}\n\n/** The route's refusal at the plan's record allowance. */\nexport function datasetDraftRecordLimitRefusal(limit: number): string {\n return `Record limit reached (${limit}) — upgrade in Billing`\n}\n\n/** One field as a caller names it. */\nexport interface DatasetDraftField {\n name: string\n /** Absent is text. */\n type?: DatasetDraftFieldType\n required?: boolean\n}\n\n/**\n * What a caller sends as `content`. Each record's values are keyed by the\n * field NAME the caller gave, since the caller has no ids yet; the writer\n * mints the ids (`roast_preference` from \"Roast preference\").\n */\nexport interface DatasetDraftContent {\n /** The dataset's name; the request's `name` stands in when absent. */\n name?: string\n fields: DatasetDraftField[]\n records?: Array<Record<string, unknown>>\n /**\n * A text field, by name, a \"Page address\" field fills from (AGL-3475):\n * asked for where a record page will show each record at its own address.\n */\n pageAddressFrom?: string\n}\n\n/** What the writer reports about a dataset. */\nexport interface DatasetDraftFacts {\n fields: Array<{ id: string; name: string; type: DatasetFieldType }>\n records: number\n /** The page-address field's id, where the dataset has one. */\n addressField: string | null\n}\n\n/** The draft as the writer will store it. */\nexport interface DatasetDraftRead {\n name: string\n model: DatasetModel\n /** Each record's values by field id, coerced and addressed. */\n records: Array<Record<string, unknown>>\n addressField: string | null\n}\n\nexport type DatasetDraftContentRead =\n | { ok: true; value: DatasetDraftRead }\n | { ok: false; problems: string[] }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** Text on one line: controls and runs of space folded to one space. */\nfunction line(value: unknown): string {\n return typeof value === 'string' ? value.replace(/[\\p{Cc}\\s]+/gu, ' ').trim() : ''\n}\n\n/** A value as a seeded record keeps it, before the model coerces it. */\nfunction seedValue(value: unknown): unknown {\n if (typeof value === 'string') return value.replace(/\\r\\n?/g, '\\n').trim().slice(0, DATASET_DRAFT_VALUE_MAX)\n if (Array.isArray(value)) {\n return value\n .map((entry) => (typeof entry === 'string' ? line(entry) : entry))\n .filter((entry) => entry !== '' && entry !== null && entry !== undefined)\n .slice(0, DATASET_DRAFT_LIST_MAX)\n }\n return value\n}\n\n/** The content as the writer reads it, or every problem that stops it. Pure. */\nexport function readDatasetDraftContent(content: Readonly<Record<string, unknown>>): DatasetDraftContentRead {\n const problems: string[] = []\n const name = line(content['name'])\n if (!name) problems.push('The dataset needs a name')\n else if (name.length > DATASET_DRAFT_NAME_MAX) {\n problems.push(`The name is longer than ${DATASET_DRAFT_NAME_MAX} characters`)\n }\n\n const rawFields = content['fields']\n const model: DatasetModel = { fields: {}, order: [] }\n /** Field name, lowercased → id. */\n const byName = new Map<string, string>()\n if (!Array.isArray(rawFields) || !rawFields.length) {\n problems.push('The dataset needs at least one field')\n } else if (rawFields.length > DATASET_DRAFT_FIELDS_MAX) {\n problems.push(`A dataset is made with at most ${DATASET_DRAFT_FIELDS_MAX} fields`)\n } else {\n for (const raw of rawFields) {\n const fieldName = isRecord(raw) ? line(raw['name']) : ''\n const rawType = (isRecord(raw) ? raw['type'] : undefined) ?? 'text'\n if (!fieldName) {\n problems.push('Each field needs a name')\n continue\n }\n if (fieldName.length > DATASET_DRAFT_FIELD_NAME_MAX) {\n problems.push(`The field \"${fieldName.slice(0, 20)}…\" has a name longer than ${DATASET_DRAFT_FIELD_NAME_MAX} characters`)\n continue\n }\n if (!(DRAFT_TYPES as readonly unknown[]).includes(rawType)) {\n problems.push(`The field \"${fieldName}\" is one of ${DRAFT_TYPES.join(', ')}`)\n continue\n }\n if (byName.has(fieldName.toLowerCase())) {\n problems.push(`Two fields are both called \"${fieldName}\"`)\n continue\n }\n const id = defaultDatasetFieldId(fieldName, new Set(model.order))\n if (!id) {\n problems.push(`The field \"${fieldName}\" needs a name with a letter in it`)\n continue\n }\n model.fields[id] = {\n name: fieldName,\n type: DATASET_DRAFT_FIELD_TYPES[rawType as DatasetDraftFieldType],\n ...(isRecord(raw) && raw['required'] === true ? { required: true } : {}),\n }\n model.order.push(id)\n byName.set(fieldName.toLowerCase(), id)\n }\n }\n\n // The page address: a text field the address fills from, once.\n let addressField: string | null = null\n const rawAddress = content['pageAddressFrom']\n if (rawAddress !== undefined && rawAddress !== null && rawAddress !== '') {\n const sourceId = byName.get(line(rawAddress).toLowerCase())\n if (!sourceId || model.fields[sourceId]?.type !== 'text') {\n problems.push('A page address fills from one of the dataset’s text fields')\n } else {\n addressField = defaultDatasetFieldId('slug', new Set(model.order))\n model.fields[addressField] = {\n name: 'Page address',\n type: 'text',\n customType: RECORD_PAGE_ADDRESS_FIELD_TYPE,\n slugFrom: sourceId,\n }\n model.order.push(addressField)\n }\n }\n\n const rawRecords = content['records'] ?? []\n const records: Array<Record<string, unknown>> = []\n if (!Array.isArray(rawRecords)) {\n problems.push('Records are a list of values by field name')\n } else if (rawRecords.length > DATASET_DRAFT_RECORDS_MAX) {\n problems.push(`A dataset is seeded with at most ${DATASET_DRAFT_RECORDS_MAX} records`)\n } else if (!problems.length) {\n rawRecords.forEach((raw, index) => {\n if (!isRecord(raw)) {\n problems.push(`Record ${index + 1} is not a set of values`)\n return\n }\n const input: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(raw)) {\n const id = byName.get(line(key).toLowerCase())\n if (!id) {\n problems.push(`Record ${index + 1} names \"${line(key).slice(0, 40)}\", which is not a field`)\n continue\n }\n input[id] = seedValue(value)\n }\n const values = coerceDocumentValues(model, input)\n // An empty value is no value: it is not stored, as the card stores none.\n for (const [id, value] of Object.entries(values)) {\n if (value === '' || value === null || value === undefined || (Array.isArray(value) && !value.length)) {\n delete values[id]\n }\n }\n const errors = validateDocument(model, values)\n for (const error of Object.values(errors)) problems.push(`Record ${index + 1}: ${error}`)\n if (!Object.keys(values).length) problems.push(`Record ${index + 1} has no values`)\n records.push(values)\n })\n }\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n\n // Every seeded record its own address, unique within the dataset.\n if (addressField) {\n const source = model.fields[addressField]?.slugFrom as string\n const assigned = uniqueRecordAddresses(records.map((values, index) => ({ id: String(index), source: values[source] })))\n records.forEach((values, index) => {\n const address = assigned.get(String(index))\n if (address) values[addressField as string] = address.slice(0, RECORD_PAGE_ADDRESS_MAX)\n })\n }\n return { ok: true, value: { name, model, records, addressField } }\n}\n\nfunction factsOf(model: DatasetModel, records: number, addressField: string | null): DatasetDraftFacts {\n return {\n fields: model.order.map((id) => ({ id, name: model.fields[id]?.name ?? id, type: model.fields[id]?.type ?? 'text' })),\n records,\n addressField,\n }\n}\n\n/** Whether content is a dataset this plugin would store, with what it says about it. Pure. */\nexport function checkDatasetDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readDatasetDraftContent(content)\n if (read.ok === false) return read\n return {\n ok: true,\n facts: factsOf(read.value.model, read.value.records.length, read.value.addressField) as unknown as Readonly<\n Record<string, unknown>\n >,\n }\n}\n\n/** Who the member is in the org, as the writer asks: their role, and whether they may manage data. */\nexport interface DatasetDraftMember {\n role: string\n canManageData: boolean\n /** The membership itself, for whether their access reaches a site. */\n member: Readonly<Record<string, unknown>>\n}\n\nexport interface DatasetDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n /** The org a site belongs to; the platform's lookup otherwise. */\n orgIdForHost?: (hostId: string) => Promise<string | null>\n /** The member, or `null` when they are not one; the platform's otherwise. */\n member?: (uid: string, orgId: string) => Promise<DatasetDraftMember | null>\n /** Whether the data store is released for the org; the platform's flag otherwise. */\n released?: (orgId: string) => Promise<boolean>\n /** Whether dataset bytes may grow; the platform's band otherwise. */\n storageFull?: (org: unknown, orgRef: FirebaseFirestore.DocumentReference) => Promise<boolean>\n}\n\n/** Roles allowed to create org data — the datasets route's, mirroring the rules' `canWriteOrgData()`. */\nconst WRITER_ROLES = new Set(['owner', 'admin', 'editor'])\n\nasync function platformMember(uid: string, orgId: string): Promise<DatasetDraftMember | null> {\n const membership = await resolveOrgMembership(uid, orgId)\n const member = membership?.member\n if (!member) return null\n return {\n role: String(member.role ?? ''),\n canManageData: await memberHasOrgPermission(orgId, member, 'data.manage'),\n member: member as unknown as Readonly<Record<string, unknown>>,\n }\n}\n\nexport function createDatasetDraftWriter(deps: DatasetDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n const orgIdForHost = deps.orgIdForHost ?? ((hostId: string) => resolveOrgIdForHost(hostId))\n const memberOf = deps.member ?? platformMember\n const released = deps.released ?? ((orgId: string) => isServerReleaseFlagOnForOrg('release_data_store', orgId))\n const storageFull =\n deps.storageFull ?? (async (org: unknown, orgRef: FirebaseFirestore.DocumentReference) => Boolean(await dataStorageRefusal(org, orgRef)))\n\n /** Everything but the room: the site is the org's, the member's role, the flag, the plan, the scope. */\n const early = async (context: PluginDraftContext): Promise<PluginDraftRefusal | null> => {\n if ((await orgIdForHost(context.hostId)) !== context.orgId) return { status: 404, error: 'Unknown site' }\n const member = await memberOf(context.uid, context.orgId)\n if (!member || !WRITER_ROLES.has(member.role)) return { status: 403, error: DATASET_DRAFT_ROLE_REFUSAL }\n if (!member.canManageData) return { status: 403, error: DATASET_DRAFT_PERMISSION_REFUSAL }\n if (!(await released(context.orgId))) return { status: 403, error: DATASET_DRAFT_RELEASE_REFUSAL }\n if (!checkEntitlement(context.org as never, 'dataStore')) return { status: 403, error: DATASET_DRAFT_PLAN_REFUSAL }\n // A member scoped to some sites must be able to see what they create.\n if (!memberCanSee(member.member as never, scopeOf(context))) {\n return { status: 403, error: DATASET_DRAFT_SCOPE_REFUSAL }\n }\n return null\n }\n\n return {\n refusal: async (context) => {\n const refused = await early(context)\n if (refused) return refused\n const used = (await firestore().collection('orgs').doc(context.orgId).collection('datasets').count().get()).data().count\n const quota = checkDatasetQuota(context.org as never, Number(used) || 0)\n return quota.allowed ? null : { status: 403, error: datasetDraftLimitRefusal(quota) }\n },\n\n check: (content) => checkDatasetDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const orgId = await orgIdForHost(hostId)\n if (!orgId) return null\n const dataset = await firestore().collection('orgs').doc(orgId).collection('datasets').doc(id).get()\n return dataset.exists ? recordOf(dataset) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n // The request's name is the one asked for; the content's stands in.\n const asked = line(request.name)\n const read = readDatasetDraftContent(asked ? { ...request.content, name: asked } : request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const refused = await early(request)\n if (refused) return { ok: false, ...refused }\n const db = firestore()\n const orgRef = db.collection('orgs').doc(request.orgId)\n const datasets = orgRef.collection('datasets')\n const datasetRef = datasets.doc(request.id)\n const { name, model, records, addressField } = read.value\n const recordQuota = checkQuota(request.org as never, 'recordsPerDataset', Math.max(0, records.length - 1))\n if (records.length && !recordQuota.allowed) {\n return { ok: false, status: 403, error: datasetDraftRecordLimitRefusal(recordQuota.limit) }\n }\n if (records.length && (await storageFull(request.org, orgRef))) {\n return { ok: false, status: 403, error: DATASET_DRAFT_STORAGE_REFUSAL }\n }\n const visibleTo = scopeOf(request)\n // Minted before the transaction, so a retried attempt writes the same rows.\n const recordIds = records.map(() => createResourceUid())\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const existing = await tx.get(datasetRef)\n // Asked again under the same id: the dataset it already wrote.\n if (existing.exists) return { ok: true, replayed: true, ...recordOf(existing) }\n const live = (await tx.get(datasets.count())).data().count\n const quota = checkDatasetQuota(request.org as never, Number(live) || 0)\n if (!quota.allowed) return { ok: false, status: 403, error: datasetDraftLimitRefusal(quota) }\n tx.create(datasetRef, {\n displayName: name,\n fields: [...model.order],\n model,\n ...newResourceScopeFields(visibleTo),\n createdAt: request.now,\n createdBy: request.uid,\n })\n records.forEach((values, order) => {\n tx.create(datasetRef.collection('records').doc(recordIds[order]), {\n values,\n ...datasetIntegrityFields(model, values),\n order,\n createdAt: request.now,\n updatedAt: request.now,\n })\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name,\n versionId: null,\n facts: factsOf(model, records.length, addressField) as unknown as Readonly<Record<string, unknown>>,\n }\n })\n },\n }\n}\n\n/** The scope a dataset made from this site takes: the org's Default sharing for it. */\nfunction scopeOf(context: Pick<PluginDraftContext, 'org' | 'hostId'>) {\n return defaultScopeForNewResource({\n defaultResourceScope: (context.org as { defaultResourceScope?: 'org' | 'host' } | null)?.defaultResourceScope,\n hostId: context.hostId,\n })\n}\n\nfunction recordOf(dataset: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (dataset.data() ?? {}) as { displayName?: unknown; model?: DatasetModel }\n const model: DatasetModel = data.model?.order ? data.model : { fields: {}, order: [] }\n const addressField = model.order.find((id) => model.fields[id]?.customType === RECORD_PAGE_ADDRESS_FIELD_TYPE) ?? null\n return {\n id: dataset.id,\n name: String(data.displayName ?? ''),\n versionId: null,\n // A replay reports the fields; how many records it was seeded with is the first write's to say.\n facts: factsOf(model, 0, addressField) as unknown as Readonly<Record<string, unknown>>,\n }\n}\n\nexport const datasetDraftWriter = createDatasetDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerDatasetDraftWriter(): void {\n registerPluginResourceDraftWriter(DATASET_DRAFT_RESOURCE, datasetDraftWriter, { pluginId: BUNDLE_ID })\n}\n"],"names":["createResourceUid","memberCanSee","checkDatasetQuota","checkEntitlement","checkQuota","defaultScopeForNewResource","newResourceScopeFields","registerPluginResourceDraftWriter","firebaseAdmin","dataStorageRefusal","memberHasOrgPermission","resolveOrgIdForHost","resolveOrgMembership","isServerReleaseFlagOnForOrg","BUNDLE_ID","coerceDocumentValues","datasetIntegrityFields","defaultDatasetFieldId","validateDocument","RECORD_PAGE_ADDRESS_FIELD_TYPE","RECORD_PAGE_ADDRESS_MAX","uniqueRecordAddresses","DATASET_DRAFT_RESOURCE","DATASET_DRAFT_NAME_MAX","DATASET_DRAFT_FIELDS_MAX","DATASET_DRAFT_FIELD_NAME_MAX","DATASET_DRAFT_RECORDS_MAX","DATASET_DRAFT_VALUE_MAX","DATASET_DRAFT_LIST_MAX","DATASET_DRAFT_FIELD_TYPES","text","number","integer","boolean","list","DRAFT_TYPES","Object","keys","DATASET_DRAFT_ROLE_REFUSAL","DATASET_DRAFT_PERMISSION_REFUSAL","DATASET_DRAFT_PLAN_REFUSAL","DATASET_DRAFT_RELEASE_REFUSAL","DATASET_DRAFT_SCOPE_REFUSAL","DATASET_DRAFT_STORAGE_REFUSAL","datasetDraftLimitRefusal","quota","upgradeRequired","limit","addonPriceUsd","datasetDraftRecordLimitRefusal","isRecord","value","Array","isArray","line","replace","trim","seedValue","slice","map","entry","filter","undefined","readDatasetDraftContent","content","problems","name","push","length","rawFields","model","fields","order","byName","Map","raw","fieldName","rawType","includes","join","has","toLowerCase","id","Set","type","required","set","addressField","rawAddress","sourceId","get","customType","slugFrom","rawRecords","records","forEach","index","input","key","entries","values","errors","error","ok","source","assigned","String","address","factsOf","checkDatasetDraftContent","read","facts","WRITER_ROLES","platformMember","uid","orgId","member","membership","role","canManageData","createDatasetDraftWriter","deps","firestore","app","orgIdForHost","hostId","memberOf","released","storageFull","org","orgRef","Boolean","early","context","status","scopeOf","refusal","refused","used","collection","doc","count","data","Number","allowed","check","dataset","exists","recordOf","write","request","asked","db","datasets","datasetRef","recordQuota","Math","max","visibleTo","recordIds","runTransaction","tx","existing","replayed","live","create","displayName","createdAt","now","createdBy","updatedAt","versionId","defaultResourceScope","find","datasetDraftWriter","registerDatasetDraftWriter","pluginId"],"mappings":"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"}
package/src/lib/server.js CHANGED
@@ -14,6 +14,8 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { registerPluginApiRoute } from "@aglyn/aglyn/server";
17
+ import { registerDatasetAiCapability } from "./server/dataset-ai-capability.js";
18
+ import { registerDatasetDraftWriter } from "./server/dataset-drafts.js";
17
19
  import { datasetsHandler } from "./server/datasets-route.js";
18
20
  import { recordPagesHandler } from "./record-pages/record-pages-route.js";
19
21
  /**
@@ -48,6 +50,12 @@ import { recordPagesHandler } from "./record-pages/record-pages-route.js";
48
50
  registerPluginApiRoute('hosts/record-pages', {
49
51
  web: recordPagesHandler
50
52
  });
53
+ // A dataset another plugin asks for by name (AGL-3616): Aglyn AI keeping a
54
+ // menu, a team or a list of services as records, under this plugin's rules.
55
+ // The console runs AI jobs (AGL-3026).
56
+ registerDatasetDraftWriter();
57
+ // …and the operation an AI build plans for it, which that writer executes.
58
+ registerDatasetAiCapability();
51
59
  }
52
60
 
53
61
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/data/src/lib/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 registerPluginApiRoute,\n type PluginApiSubjectResolver,\n} from '@aglyn/aglyn/server'\nimport { datasetsHandler } from './server/datasets-route'\nimport { recordPagesHandler } from './record-pages/record-pages-route'\n\n/**\n * The organization a datasets request is for, read from the request the way\n * the handler reads it — the JSON body's `orgId` (or `?orgId=`). Datasets belong to the organization and name no site, so without\n * this the dispatcher's release gate would read the request as anonymous and\n * refuse it under a partial rollout of the data store.\n */\nconst orgSubject: PluginApiSubjectResolver = async (request) => {\n const fromQuery = new URL(request.url).searchParams.get('orgId')\n if (fromQuery) return { orgId: fromQuery }\n if (request.method === 'GET' || request.method === 'HEAD') return null\n const body = (await request.json().catch(() => null)) as {\n orgId?: unknown\n } | null\n return typeof body?.orgId === 'string' && body.orgId\n ? { orgId: body.orgId }\n : null\n}\n\n/**\n * Console API: the organization's datasets — creating a dataset and its\n * records within the plan (`POST /api/orgs/datasets`) — and a site's record\n * templates (`POST /api/hosts/record-pages`). A dataset's import and export\n * run on the platform's transfer routes (`/api/transfer/*`, AGL-3530). Served by the console's plugin\n * dispatcher, at the addresses the console always answered them on.\n */\nexport function registerDataConsoleApi(): void {\n registerPluginApiRoute('orgs/datasets', { web: datasetsHandler }, {\n subject: orgSubject,\n })\n // A site's record templates (AGL-3475): `POST /api/hosts/record-pages`.\n registerPluginApiRoute('hosts/record-pages', { web: recordPagesHandler })\n}\n"],"names":["registerPluginApiRoute","datasetsHandler","recordPagesHandler","orgSubject","request","fromQuery","URL","url","searchParams","get","orgId","method","body","json","catch","registerDataConsoleApi","web","subject"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,sBAAsB,QAEjB,sBAAqB;AAC5B,SAASC,eAAe,QAAQ,6BAAyB;AACzD,SAASC,kBAAkB,QAAQ,uCAAmC;AAEtE;;;;;CAKC,GACD,MAAMC,aAAuC,OAAOC;IAClD,MAAMC,YAAY,IAAIC,IAAIF,QAAQG,GAAG,EAAEC,YAAY,CAACC,GAAG,CAAC;IACxD,IAAIJ,WAAW,OAAO;QAAEK,OAAOL;IAAU;IACzC,IAAID,QAAQO,MAAM,KAAK,SAASP,QAAQO,MAAM,KAAK,QAAQ,OAAO;IAClE,MAAMC,OAAQ,MAAMR,QAAQS,IAAI,GAAGC,KAAK,CAAC,IAAM;IAG/C,OAAO,QAAOF,wBAAAA,KAAMF,KAAK,MAAK,YAAYE,KAAKF,KAAK,GAChD;QAAEA,OAAOE,KAAKF,KAAK;IAAC,IACpB;AACN;AAEA;;;;;;CAMC,GACD,OAAO,SAASK;IACdf,uBAAuB,iBAAiB;QAAEgB,KAAKf;IAAgB,GAAG;QAChEgB,SAASd;IACX;IACA,wEAAwE;IACxEH,uBAAuB,sBAAsB;QAAEgB,KAAKd;IAAmB;AACzE"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/data/src/lib/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 registerPluginApiRoute,\n type PluginApiSubjectResolver,\n} from '@aglyn/aglyn/server'\nimport { registerDatasetAiCapability } from './server/dataset-ai-capability'\nimport { registerDatasetDraftWriter } from './server/dataset-drafts'\nimport { datasetsHandler } from './server/datasets-route'\nimport { recordPagesHandler } from './record-pages/record-pages-route'\n\n/**\n * The organization a datasets request is for, read from the request the way\n * the handler reads it — the JSON body's `orgId` (or `?orgId=`). Datasets belong to the organization and name no site, so without\n * this the dispatcher's release gate would read the request as anonymous and\n * refuse it under a partial rollout of the data store.\n */\nconst orgSubject: PluginApiSubjectResolver = async (request) => {\n const fromQuery = new URL(request.url).searchParams.get('orgId')\n if (fromQuery) return { orgId: fromQuery }\n if (request.method === 'GET' || request.method === 'HEAD') return null\n const body = (await request.json().catch(() => null)) as {\n orgId?: unknown\n } | null\n return typeof body?.orgId === 'string' && body.orgId\n ? { orgId: body.orgId }\n : null\n}\n\n/**\n * Console API: the organization's datasets — creating a dataset and its\n * records within the plan (`POST /api/orgs/datasets`) — and a site's record\n * templates (`POST /api/hosts/record-pages`). A dataset's import and export\n * run on the platform's transfer routes (`/api/transfer/*`, AGL-3530). Served by the console's plugin\n * dispatcher, at the addresses the console always answered them on.\n */\nexport function registerDataConsoleApi(): void {\n registerPluginApiRoute('orgs/datasets', { web: datasetsHandler }, {\n subject: orgSubject,\n })\n // A site's record templates (AGL-3475): `POST /api/hosts/record-pages`.\n registerPluginApiRoute('hosts/record-pages', { web: recordPagesHandler })\n // A dataset another plugin asks for by name (AGL-3616): Aglyn AI keeping a\n // menu, a team or a list of services as records, under this plugin's rules.\n // The console runs AI jobs (AGL-3026).\n registerDatasetDraftWriter()\n // …and the operation an AI build plans for it, which that writer executes.\n registerDatasetAiCapability()\n}\n"],"names":["registerPluginApiRoute","registerDatasetAiCapability","registerDatasetDraftWriter","datasetsHandler","recordPagesHandler","orgSubject","request","fromQuery","URL","url","searchParams","get","orgId","method","body","json","catch","registerDataConsoleApi","web","subject"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,sBAAsB,QAEjB,sBAAqB;AAC5B,SAASC,2BAA2B,QAAQ,oCAAgC;AAC5E,SAASC,0BAA0B,QAAQ,6BAAyB;AACpE,SAASC,eAAe,QAAQ,6BAAyB;AACzD,SAASC,kBAAkB,QAAQ,uCAAmC;AAEtE;;;;;CAKC,GACD,MAAMC,aAAuC,OAAOC;IAClD,MAAMC,YAAY,IAAIC,IAAIF,QAAQG,GAAG,EAAEC,YAAY,CAACC,GAAG,CAAC;IACxD,IAAIJ,WAAW,OAAO;QAAEK,OAAOL;IAAU;IACzC,IAAID,QAAQO,MAAM,KAAK,SAASP,QAAQO,MAAM,KAAK,QAAQ,OAAO;IAClE,MAAMC,OAAQ,MAAMR,QAAQS,IAAI,GAAGC,KAAK,CAAC,IAAM;IAG/C,OAAO,QAAOF,wBAAAA,KAAMF,KAAK,MAAK,YAAYE,KAAKF,KAAK,GAChD;QAAEA,OAAOE,KAAKF,KAAK;IAAC,IACpB;AACN;AAEA;;;;;;CAMC,GACD,OAAO,SAASK;IACdjB,uBAAuB,iBAAiB;QAAEkB,KAAKf;IAAgB,GAAG;QAChEgB,SAASd;IACX;IACA,wEAAwE;IACxEL,uBAAuB,sBAAsB;QAAEkB,KAAKd;IAAmB;IACvE,2EAA2E;IAC3E,4EAA4E;IAC5E,uCAAuC;IACvCF;IACA,2EAA2E;IAC3ED;AACF"}