@aglyn/plugins-data 1.0.0-beta.240 → 1.0.0-beta.241
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 +10 -10
- package/src/lib/declarations.server.js +3 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/dataset-image-field.d.ts +36 -0
- package/src/lib/model/dataset-image-field.js +47 -0
- package/src/lib/model/dataset-image-field.js.map +1 -0
- package/src/lib/model/dataset-record.js +7 -4
- package/src/lib/model/dataset-record.js.map +1 -1
- package/src/lib/plugin.js +3 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/dataset-drafts.d.ts +9 -1
- package/src/lib/server/dataset-drafts.js +27 -6
- package/src/lib/server/dataset-drafts.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-data",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.241",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,15 +25,15 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
34
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
35
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
36
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.241",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.241",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.241",
|
|
31
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.241",
|
|
32
|
+
"@aglyn/shared-util-http": "1.0.0-beta.241",
|
|
33
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.241",
|
|
34
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.241",
|
|
35
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.241",
|
|
36
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.241",
|
|
37
37
|
"@swc/helpers": "0.5.23"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
@@ -24,6 +24,7 @@ import { registerCustomFieldType } from "@aglyn/aglyn/plugin-manager/custom-fiel
|
|
|
24
24
|
import { registerPluginSitemapReader } from "@aglyn/aglyn/plugin-manager/plugin-sitemap-readers";
|
|
25
25
|
import { registerPluginLivePaths } from "@aglyn/aglyn/plugin-manager/plugin-live-paths";
|
|
26
26
|
import { BUNDLE_ID, DATASET_REPEAT_SOURCE_ID, DATASET_STEP_TYPES, DATASET_STORAGE_METER_ID } from "./constants/bundle-common.js";
|
|
27
|
+
import { DATASET_IMAGE_FIELD } from "./model/dataset-image-field.js";
|
|
27
28
|
import { RECORD_PAGE_ADDRESS_FIELD } from "./record-pages/record-pages.js";
|
|
28
29
|
import { datasetsSitePackage } from "./site-bundle/datasets-package.js";
|
|
29
30
|
/**
|
|
@@ -94,6 +95,8 @@ import { datasetsSitePackage } from "./site-bundle/datasets-package.js";
|
|
|
94
95
|
// A record's page address (AGL-3475) is checked on every server path that
|
|
95
96
|
// writes a record, the tenant's form and automation writes included.
|
|
96
97
|
registerCustomFieldType(RECORD_PAGE_ADDRESS_FIELD);
|
|
98
|
+
// A record's photo (AGL-3616), the address an Image binds, checked the same way.
|
|
99
|
+
registerCustomFieldType(DATASET_IMAGE_FIELD);
|
|
97
100
|
// Record pages in the sitemap and `/llms.txt` (AGL-3475), declared as the
|
|
98
101
|
// `records` family in `plugins.config.json`; the reads load with the first
|
|
99
102
|
// sitemap a crawler asks for.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/data/src/lib/declarations.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\n// The seam from its own module, not the plugin-manager barrel: boot needs the\n// registry and nothing else, and the barrel reaches the client contexts.\nimport { registerFormRecordTarget } from '@aglyn/aglyn/plugin-manager/submission-record-target'\nimport { registerRepeatRowReader } from '@aglyn/aglyn/plugin-manager/repeat-rows'\nimport { registerPluginSiteBundleSection } from '@aglyn/aglyn/plugin-manager/plugin-site-bundle'\nimport { registerPluginUsageMeter } from '@aglyn/aglyn/plugin-manager/plugin-usage-meters'\nimport { registerServerStepExecutor } from '@aglyn/aglyn/plugin-manager/plugin-server-steps'\nimport { registerCustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields'\nimport { registerPluginSitemapReader } from '@aglyn/aglyn/plugin-manager/plugin-sitemap-readers'\nimport { registerPluginLivePaths } from '@aglyn/aglyn/plugin-manager/plugin-live-paths'\nimport {\n BUNDLE_ID,\n DATASET_REPEAT_SOURCE_ID,\n DATASET_STEP_TYPES,\n DATASET_STORAGE_METER_ID,\n} from './constants/bundle-common'\nimport { RECORD_PAGE_ADDRESS_FIELD } from './record-pages/record-pages'\nimport { datasetsSitePackage } from './site-bundle/datasets-package'\n\n/**\n * The data plugin's server declarations: the light registrations core reads\n * at boot, before any surface loads.\n *\n * A published page that repeats an element over a dataset asks the platform\n * for the rows, and this is who answers — in every process that composes a\n * page, the tenant's and the console's previews alike. The reader, and the\n * Admin SDK with it, loads on the first page that repeats, not at boot.\n * `plugins.config.json` declares the same source (`repeatSource`), so a boot\n * that skipped this is refused rather than rendered as \"no rows\".\n *\n * A form can also file every submission as a dataset record, and this is the\n * target that stamps such a form with its signed binding when a page renders\n * and writes the record when the submission comes back. Declared the same way\n * (`formRecordTarget`), and just as light: both halves load on first use.\n *\n * A site's datasets are also its section of the whole-site backup: the\n * console's export and restore ask this plugin for them. Declared too\n * (`siteBundleSections`), so a boot that skipped this fails the export rather\n * than shipping a backup without them; the answers load when a backup is made\n * or restored.\n *\n * And the monthly usage sweep, a core cron, measures what the datasets store\n * through this plugin's meter — declared in `usageAxes`, so the sweep refuses\n * to bill a month without it. Its reads load with the first sweep.\n *\n * An automation that writes a dataset record runs that step through this\n * plugin: the workflows engine reaches it through the platform's server-step\n * seam, in whichever process runs the automation. Declared too (`serverSteps`),\n * so a boot that skipped this fails the step with its reason rather than\n * reporting a record that was never written; the writes load with the first\n * step.\n *\n * And the \"Page address\" field type a record template reads a record's page\n * from, so every write path holds an address to the shape a URL can carry, and\n * the reader that lists record pages in the sitemap — declared too\n * (`sitemapReaders`), so a boot that skipped this degrades the sitemap rather\n * than dropping every record page from it.\n */\nexport function registerDataServerDeclarations(): void {\n registerRepeatRowReader(\n DATASET_REPEAT_SOURCE_ID,\n async (request) =>\n (await import('./repeat/dataset-repeat-rows.server')).readPublishedDatasetRows(\n request,\n ),\n { pluginId: BUNDLE_ID },\n )\n registerFormRecordTarget(\n {\n stamp: async (nodes, hostId) =>\n (await import('./form-target/stamp-form-dataset-bindings')).stampFormDatasetBindings(\n nodes,\n hostId,\n ),\n write: async (request) =>\n (await import('./form-target/dataset-form-record-target.server')).writeFormSubmissionRecord(\n request,\n ),\n },\n { pluginId: BUNDLE_ID },\n )\n const datasets = () => import('./site-bundle/datasets-site-bundle.server')\n registerPluginSiteBundleSection(\n 'datasets',\n {\n export: async (request) => (await datasets()).exportSiteDatasets(request),\n refusal: async (request) => (await datasets()).siteDatasetsRefusal(request),\n import: async (request) => (await datasets()).importSiteDatasets(request),\n package: datasetsSitePackage,\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginUsageMeter({\n pluginId: BUNDLE_ID,\n id: DATASET_STORAGE_METER_ID,\n measure: async (context) =>\n (await import('./server/dataset-storage-meter')).measureDatasetStorage(context),\n })\n registerServerStepExecutor(\n DATASET_STEP_TYPES,\n async (request) => (await import('./server/dataset-steps.server')).runDatasetStep(request),\n { pluginId: BUNDLE_ID },\n )\n // A record's page address (AGL-3475) is checked on every server path that\n // writes a record, the tenant's form and automation writes included.\n registerCustomFieldType(RECORD_PAGE_ADDRESS_FIELD)\n // Record pages in the sitemap and `/llms.txt` (AGL-3475), declared as the\n // `records` family in `plugins.config.json`; the reads load with the first\n // sitemap a crawler asks for.\n const sitemap = async () =>\n (await import('./record-pages/record-page-sitemap.server')).recordPagesSitemapReader\n registerPluginSitemapReader(\n 'records',\n {\n children: async (request) => (await sitemap()).children(request),\n urls: async (request) => (await sitemap()).urls(request),\n listings: async (request) => (await sitemap()).listings?.(request) ?? [],\n },\n { pluginId: BUNDLE_ID },\n )\n // And the record pages a publish makes stale, which no routing-map entry\n // names (AGL-3475).\n registerPluginLivePaths(\n async (request) =>\n (await import('./record-pages/record-page-live-paths.server')).recordPageLivePaths(\n request,\n ),\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["registerFormRecordTarget","registerRepeatRowReader","registerPluginSiteBundleSection","registerPluginUsageMeter","registerServerStepExecutor","registerCustomFieldType","registerPluginSitemapReader","registerPluginLivePaths","BUNDLE_ID","DATASET_REPEAT_SOURCE_ID","DATASET_STEP_TYPES","DATASET_STORAGE_METER_ID","RECORD_PAGE_ADDRESS_FIELD","datasetsSitePackage","registerDataServerDeclarations","request","readPublishedDatasetRows","pluginId","stamp","nodes","hostId","stampFormDatasetBindings","write","writeFormSubmissionRecord","datasets","export","exportSiteDatasets","refusal","siteDatasetsRefusal","import","importSiteDatasets","package","id","measure","context","measureDatasetStorage","runDatasetStep","sitemap","recordPagesSitemapReader","children","urls","listings","recordPageLivePaths"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,yEAAyE;AACzE,SAASA,wBAAwB,QAAQ,uDAAsD;AAC/F,SAASC,uBAAuB,QAAQ,0CAAyC;AACjF,SAASC,+BAA+B,QAAQ,iDAAgD;AAChG,SAASC,wBAAwB,QAAQ,kDAAiD;AAC1F,SAASC,0BAA0B,QAAQ,kDAAiD;AAC5F,SAASC,uBAAuB,QAAQ,4CAA2C;AACnF,SAASC,2BAA2B,QAAQ,qDAAoD;AAChG,SAASC,uBAAuB,QAAQ,gDAA+C;AACvF,SACEC,SAAS,EACTC,wBAAwB,EACxBC,kBAAkB,EAClBC,wBAAwB,QACnB,+BAA2B;AAClC,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,mBAAmB,QAAQ,oCAAgC;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCC,GACD,OAAO,SAASC;
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/data/src/lib/declarations.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\n// The seam from its own module, not the plugin-manager barrel: boot needs the\n// registry and nothing else, and the barrel reaches the client contexts.\nimport { registerFormRecordTarget } from '@aglyn/aglyn/plugin-manager/submission-record-target'\nimport { registerRepeatRowReader } from '@aglyn/aglyn/plugin-manager/repeat-rows'\nimport { registerPluginSiteBundleSection } from '@aglyn/aglyn/plugin-manager/plugin-site-bundle'\nimport { registerPluginUsageMeter } from '@aglyn/aglyn/plugin-manager/plugin-usage-meters'\nimport { registerServerStepExecutor } from '@aglyn/aglyn/plugin-manager/plugin-server-steps'\nimport { registerCustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields'\nimport { registerPluginSitemapReader } from '@aglyn/aglyn/plugin-manager/plugin-sitemap-readers'\nimport { registerPluginLivePaths } from '@aglyn/aglyn/plugin-manager/plugin-live-paths'\nimport {\n BUNDLE_ID,\n DATASET_REPEAT_SOURCE_ID,\n DATASET_STEP_TYPES,\n DATASET_STORAGE_METER_ID,\n} from './constants/bundle-common'\nimport { DATASET_IMAGE_FIELD } from './model/dataset-image-field'\nimport { RECORD_PAGE_ADDRESS_FIELD } from './record-pages/record-pages'\nimport { datasetsSitePackage } from './site-bundle/datasets-package'\n\n/**\n * The data plugin's server declarations: the light registrations core reads\n * at boot, before any surface loads.\n *\n * A published page that repeats an element over a dataset asks the platform\n * for the rows, and this is who answers — in every process that composes a\n * page, the tenant's and the console's previews alike. The reader, and the\n * Admin SDK with it, loads on the first page that repeats, not at boot.\n * `plugins.config.json` declares the same source (`repeatSource`), so a boot\n * that skipped this is refused rather than rendered as \"no rows\".\n *\n * A form can also file every submission as a dataset record, and this is the\n * target that stamps such a form with its signed binding when a page renders\n * and writes the record when the submission comes back. Declared the same way\n * (`formRecordTarget`), and just as light: both halves load on first use.\n *\n * A site's datasets are also its section of the whole-site backup: the\n * console's export and restore ask this plugin for them. Declared too\n * (`siteBundleSections`), so a boot that skipped this fails the export rather\n * than shipping a backup without them; the answers load when a backup is made\n * or restored.\n *\n * And the monthly usage sweep, a core cron, measures what the datasets store\n * through this plugin's meter — declared in `usageAxes`, so the sweep refuses\n * to bill a month without it. Its reads load with the first sweep.\n *\n * An automation that writes a dataset record runs that step through this\n * plugin: the workflows engine reaches it through the platform's server-step\n * seam, in whichever process runs the automation. Declared too (`serverSteps`),\n * so a boot that skipped this fails the step with its reason rather than\n * reporting a record that was never written; the writes load with the first\n * step.\n *\n * And the \"Page address\" field type a record template reads a record's page\n * from, so every write path holds an address to the shape a URL can carry, and\n * the reader that lists record pages in the sitemap — declared too\n * (`sitemapReaders`), so a boot that skipped this degrades the sitemap rather\n * than dropping every record page from it.\n */\nexport function registerDataServerDeclarations(): void {\n registerRepeatRowReader(\n DATASET_REPEAT_SOURCE_ID,\n async (request) =>\n (await import('./repeat/dataset-repeat-rows.server')).readPublishedDatasetRows(\n request,\n ),\n { pluginId: BUNDLE_ID },\n )\n registerFormRecordTarget(\n {\n stamp: async (nodes, hostId) =>\n (await import('./form-target/stamp-form-dataset-bindings')).stampFormDatasetBindings(\n nodes,\n hostId,\n ),\n write: async (request) =>\n (await import('./form-target/dataset-form-record-target.server')).writeFormSubmissionRecord(\n request,\n ),\n },\n { pluginId: BUNDLE_ID },\n )\n const datasets = () => import('./site-bundle/datasets-site-bundle.server')\n registerPluginSiteBundleSection(\n 'datasets',\n {\n export: async (request) => (await datasets()).exportSiteDatasets(request),\n refusal: async (request) => (await datasets()).siteDatasetsRefusal(request),\n import: async (request) => (await datasets()).importSiteDatasets(request),\n package: datasetsSitePackage,\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginUsageMeter({\n pluginId: BUNDLE_ID,\n id: DATASET_STORAGE_METER_ID,\n measure: async (context) =>\n (await import('./server/dataset-storage-meter')).measureDatasetStorage(context),\n })\n registerServerStepExecutor(\n DATASET_STEP_TYPES,\n async (request) => (await import('./server/dataset-steps.server')).runDatasetStep(request),\n { pluginId: BUNDLE_ID },\n )\n // A record's page address (AGL-3475) is checked on every server path that\n // writes a record, the tenant's form and automation writes included.\n registerCustomFieldType(RECORD_PAGE_ADDRESS_FIELD)\n // A record's photo (AGL-3616), the address an Image binds, checked the same way.\n registerCustomFieldType(DATASET_IMAGE_FIELD)\n // Record pages in the sitemap and `/llms.txt` (AGL-3475), declared as the\n // `records` family in `plugins.config.json`; the reads load with the first\n // sitemap a crawler asks for.\n const sitemap = async () =>\n (await import('./record-pages/record-page-sitemap.server')).recordPagesSitemapReader\n registerPluginSitemapReader(\n 'records',\n {\n children: async (request) => (await sitemap()).children(request),\n urls: async (request) => (await sitemap()).urls(request),\n listings: async (request) => (await sitemap()).listings?.(request) ?? [],\n },\n { pluginId: BUNDLE_ID },\n )\n // And the record pages a publish makes stale, which no routing-map entry\n // names (AGL-3475).\n registerPluginLivePaths(\n async (request) =>\n (await import('./record-pages/record-page-live-paths.server')).recordPageLivePaths(\n request,\n ),\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["registerFormRecordTarget","registerRepeatRowReader","registerPluginSiteBundleSection","registerPluginUsageMeter","registerServerStepExecutor","registerCustomFieldType","registerPluginSitemapReader","registerPluginLivePaths","BUNDLE_ID","DATASET_REPEAT_SOURCE_ID","DATASET_STEP_TYPES","DATASET_STORAGE_METER_ID","DATASET_IMAGE_FIELD","RECORD_PAGE_ADDRESS_FIELD","datasetsSitePackage","registerDataServerDeclarations","request","readPublishedDatasetRows","pluginId","stamp","nodes","hostId","stampFormDatasetBindings","write","writeFormSubmissionRecord","datasets","export","exportSiteDatasets","refusal","siteDatasetsRefusal","import","importSiteDatasets","package","id","measure","context","measureDatasetStorage","runDatasetStep","sitemap","recordPagesSitemapReader","children","urls","listings","recordPageLivePaths"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,yEAAyE;AACzE,SAASA,wBAAwB,QAAQ,uDAAsD;AAC/F,SAASC,uBAAuB,QAAQ,0CAAyC;AACjF,SAASC,+BAA+B,QAAQ,iDAAgD;AAChG,SAASC,wBAAwB,QAAQ,kDAAiD;AAC1F,SAASC,0BAA0B,QAAQ,kDAAiD;AAC5F,SAASC,uBAAuB,QAAQ,4CAA2C;AACnF,SAASC,2BAA2B,QAAQ,qDAAoD;AAChG,SAASC,uBAAuB,QAAQ,gDAA+C;AACvF,SACEC,SAAS,EACTC,wBAAwB,EACxBC,kBAAkB,EAClBC,wBAAwB,QACnB,+BAA2B;AAClC,SAASC,mBAAmB,QAAQ,iCAA6B;AACjE,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,mBAAmB,QAAQ,oCAAgC;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCC,GACD,OAAO,SAASC;IACdd,wBACEQ,0BACA,OAAOO,UACL,AAAC,CAAA,MAAM,MAAM,CAAC,yCAAqC,EAAGC,wBAAwB,CAC5ED,UAEJ;QAAEE,UAAUV;IAAU;IAExBR,yBACE;QACEmB,OAAO,OAAOC,OAAOC,SACnB,AAAC,CAAA,MAAM,MAAM,CAAC,+CAA2C,EAAGC,wBAAwB,CAClFF,OACAC;QAEJE,OAAO,OAAOP,UACZ,AAAC,CAAA,MAAM,MAAM,CAAC,qDAAiD,EAAGQ,yBAAyB,CACzFR;IAEN,GACA;QAAEE,UAAUV;IAAU;IAExB,MAAMiB,WAAW,IAAM,MAAM,CAAC;IAC9BvB,gCACE,YACA;QACEwB,QAAQ,OAAOV,UAAY,AAAC,CAAA,MAAMS,UAAS,EAAGE,kBAAkB,CAACX;QACjEY,SAAS,OAAOZ,UAAY,AAAC,CAAA,MAAMS,UAAS,EAAGI,mBAAmB,CAACb;QACnEc,QAAQ,OAAOd,UAAY,AAAC,CAAA,MAAMS,UAAS,EAAGM,kBAAkB,CAACf;QACjEgB,SAASlB;IACX,GACA;QAAEI,UAAUV;IAAU;IAExBL,yBAAyB;QACvBe,UAAUV;QACVyB,IAAItB;QACJuB,SAAS,OAAOC,UACd,AAAC,CAAA,MAAM,MAAM,CAAC,oCAAgC,EAAGC,qBAAqB,CAACD;IAC3E;IACA/B,2BACEM,oBACA,OAAOM,UAAY,AAAC,CAAA,MAAM,MAAM,CAAC,mCAA+B,EAAGqB,cAAc,CAACrB,UAClF;QAAEE,UAAUV;IAAU;IAExB,0EAA0E;IAC1E,qEAAqE;IACrEH,wBAAwBQ;IACxB,iFAAiF;IACjFR,wBAAwBO;IACxB,0EAA0E;IAC1E,2EAA2E;IAC3E,8BAA8B;IAC9B,MAAM0B,UAAU,UACd,AAAC,CAAA,MAAM,MAAM,CAAC,+CAA2C,EAAGC,wBAAwB;IACtFjC,4BACE,WACA;QACEkC,UAAU,OAAOxB,UAAY,AAAC,CAAA,MAAMsB,SAAQ,EAAGE,QAAQ,CAACxB;QACxDyB,MAAM,OAAOzB,UAAY,AAAC,CAAA,MAAMsB,SAAQ,EAAGG,IAAI,CAACzB;QAChD0B,UAAU,OAAO1B;;gBAAY,WAAC;4BAAD,YAAA,CAAC,QAAA,MAAMsB,WAAWI,QAAQ,qBAA1B,eAAC,OAA4B1B,2BAAY,EAAE;;IAC1E,GACA;QAAEE,UAAUV;IAAU;IAExB,yEAAyE;IACzE,oBAAoB;IACpBD,wBACE,OAAOS,UACL,AAAC,CAAA,MAAM,MAAM,CAAC,kDAA8C,EAAG2B,mBAAmB,CAChF3B,UAEJ;QAAEE,UAAUV;IAAU;AAE1B"}
|
|
@@ -0,0 +1,36 @@
|
|
|
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 { CustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields';
|
|
18
|
+
/**
|
|
19
|
+
* A RECORD'S PHOTO (AGL-3616): the "Image" field type, riding `text` storage.
|
|
20
|
+
*
|
|
21
|
+
* A portfolio's pieces, a team, a menu, a studio's classes are lists a
|
|
22
|
+
* visitor looks at before reading, so their records carry a photo, and the
|
|
23
|
+
* page that repeats over them binds it as an Image's `src`
|
|
24
|
+
* (`{{item.<field>}}`). The value is an address the site's own Image element
|
|
25
|
+
* shows: the media library's CDN path (`/api/media/cdn/{scope}/{mediaId}`,
|
|
26
|
+
* the form a picker writes into a product), a `media:` reference, another
|
|
27
|
+
* path on the site, or an https address. Nothing else is a picture a page can
|
|
28
|
+
* draw, so nothing else is stored.
|
|
29
|
+
*/
|
|
30
|
+
/** The custom field type a record's photo rides. Persisted on field definitions — never rename. */
|
|
31
|
+
export declare const DATASET_IMAGE_FIELD_TYPE = "image";
|
|
32
|
+
/** The longest photo address kept. */
|
|
33
|
+
export declare const DATASET_IMAGE_VALUE_MAX = 2000;
|
|
34
|
+
/** Whether a value is a photo address a record may hold. */
|
|
35
|
+
export declare function isDatasetImageValue(value: unknown): value is string;
|
|
36
|
+
export declare const DATASET_IMAGE_FIELD: CustomFieldType;
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
17
|
+
/**
|
|
18
|
+
* A RECORD'S PHOTO (AGL-3616): the "Image" field type, riding `text` storage.
|
|
19
|
+
*
|
|
20
|
+
* A portfolio's pieces, a team, a menu, a studio's classes are lists a
|
|
21
|
+
* visitor looks at before reading, so their records carry a photo, and the
|
|
22
|
+
* page that repeats over them binds it as an Image's `src`
|
|
23
|
+
* (`{{item.<field>}}`). The value is an address the site's own Image element
|
|
24
|
+
* shows: the media library's CDN path (`/api/media/cdn/{scope}/{mediaId}`,
|
|
25
|
+
* the form a picker writes into a product), a `media:` reference, another
|
|
26
|
+
* path on the site, or an https address. Nothing else is a picture a page can
|
|
27
|
+
* draw, so nothing else is stored.
|
|
28
|
+
*/ /** The custom field type a record's photo rides. Persisted on field definitions — never rename. */ export const DATASET_IMAGE_FIELD_TYPE = 'image';
|
|
29
|
+
/** The longest photo address kept. */ export const DATASET_IMAGE_VALUE_MAX = 2000;
|
|
30
|
+
const MEDIA_REF = /^media:[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+(?:@[A-Za-z0-9]+)?$/;
|
|
31
|
+
/** Whether a value is a photo address a record may hold. */ export function isDatasetImageValue(value) {
|
|
32
|
+
if (typeof value !== 'string' || !value || value.length > DATASET_IMAGE_VALUE_MAX) return false;
|
|
33
|
+
if (/[\s"'<>\\]/.test(value)) return false;
|
|
34
|
+
if (value.startsWith('media:')) return MEDIA_REF.test(value);
|
|
35
|
+
if (value.startsWith('/')) return !value.startsWith('//');
|
|
36
|
+
return /^https:\/\/[^/]+/.test(value);
|
|
37
|
+
}
|
|
38
|
+
export const DATASET_IMAGE_FIELD = {
|
|
39
|
+
name: DATASET_IMAGE_FIELD_TYPE,
|
|
40
|
+
pluginId: BUNDLE_ID,
|
|
41
|
+
label: 'Image',
|
|
42
|
+
baseType: 'text',
|
|
43
|
+
description: 'A photo from the media library, or an https link to one',
|
|
44
|
+
validate: (value)=>isDatasetImageValue(value) ? null : 'Use a photo from the media library or an https link'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=dataset-image-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/dataset-image-field.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 type { CustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * A RECORD'S PHOTO (AGL-3616): the \"Image\" field type, riding `text` storage.\n *\n * A portfolio's pieces, a team, a menu, a studio's classes are lists a\n * visitor looks at before reading, so their records carry a photo, and the\n * page that repeats over them binds it as an Image's `src`\n * (`{{item.<field>}}`). The value is an address the site's own Image element\n * shows: the media library's CDN path (`/api/media/cdn/{scope}/{mediaId}`,\n * the form a picker writes into a product), a `media:` reference, another\n * path on the site, or an https address. Nothing else is a picture a page can\n * draw, so nothing else is stored.\n */\n\n/** The custom field type a record's photo rides. Persisted on field definitions — never rename. */\nexport const DATASET_IMAGE_FIELD_TYPE = 'image'\n\n/** The longest photo address kept. */\nexport const DATASET_IMAGE_VALUE_MAX = 2_000\n\nconst MEDIA_REF = /^media:[A-Za-z0-9_-]+\\/[A-Za-z0-9_-]+(?:@[A-Za-z0-9]+)?$/\n\n/** Whether a value is a photo address a record may hold. */\nexport function isDatasetImageValue(value: unknown): value is string {\n if (typeof value !== 'string' || !value || value.length > DATASET_IMAGE_VALUE_MAX) return false\n if (/[\\s\"'<>\\\\]/.test(value)) return false\n if (value.startsWith('media:')) return MEDIA_REF.test(value)\n if (value.startsWith('/')) return !value.startsWith('//')\n return /^https:\\/\\/[^/]+/.test(value)\n}\n\nexport const DATASET_IMAGE_FIELD: CustomFieldType = {\n name: DATASET_IMAGE_FIELD_TYPE,\n pluginId: BUNDLE_ID,\n label: 'Image',\n baseType: 'text',\n description: 'A photo from the media library, or an https link to one',\n validate: (value) => (isDatasetImageValue(value) ? null : 'Use a photo from the media library or an https link'),\n}\n"],"names":["BUNDLE_ID","DATASET_IMAGE_FIELD_TYPE","DATASET_IMAGE_VALUE_MAX","MEDIA_REF","isDatasetImageValue","value","length","test","startsWith","DATASET_IMAGE_FIELD","name","pluginId","label","baseType","description","validate"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,SAAS,QAAQ,gCAA4B;AAEtD;;;;;;;;;;;CAWC,GAED,iGAAiG,GACjG,OAAO,MAAMC,2BAA2B,QAAO;AAE/C,oCAAoC,GACpC,OAAO,MAAMC,0BAA0B,KAAK;AAE5C,MAAMC,YAAY;AAElB,0DAA0D,GAC1D,OAAO,SAASC,oBAAoBC,KAAc;IAChD,IAAI,OAAOA,UAAU,YAAY,CAACA,SAASA,MAAMC,MAAM,GAAGJ,yBAAyB,OAAO;IAC1F,IAAI,aAAaK,IAAI,CAACF,QAAQ,OAAO;IACrC,IAAIA,MAAMG,UAAU,CAAC,WAAW,OAAOL,UAAUI,IAAI,CAACF;IACtD,IAAIA,MAAMG,UAAU,CAAC,MAAM,OAAO,CAACH,MAAMG,UAAU,CAAC;IACpD,OAAO,mBAAmBD,IAAI,CAACF;AACjC;AAEA,OAAO,MAAMI,sBAAuC;IAClDC,MAAMT;IACNU,UAAUX;IACVY,OAAO;IACPC,UAAU;IACVC,aAAa;IACbC,UAAU,CAACV,QAAWD,oBAAoBC,SAAS,OAAO;AAC5D,EAAC"}
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { effectiveDatasetModel } from "./dataset-models.js";
|
|
17
18
|
import { datasetDisplayName } from "./datasets.js";
|
|
18
19
|
/**
|
|
19
20
|
* One stored dataset as this plugin shares it with another (AGL-3080) — the
|
|
@@ -39,12 +40,14 @@ import { datasetDisplayName } from "./datasets.js";
|
|
|
39
40
|
facts: {
|
|
40
41
|
fields: model.order.map((fieldId)=>{
|
|
41
42
|
var _ref;
|
|
42
|
-
var _model_fields_fieldId, _model_fields_fieldId1;
|
|
43
|
-
return {
|
|
43
|
+
var _model_fields_fieldId, _model_fields_fieldId1, _model_fields_fieldId2, _model_fields_fieldId3;
|
|
44
|
+
return _extends({
|
|
44
45
|
id: fieldId,
|
|
45
46
|
name: text((_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId.name) || fieldId,
|
|
46
47
|
type: String((_ref = (_model_fields_fieldId1 = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId1.type) != null ? _ref : 'text')
|
|
47
|
-
}
|
|
48
|
+
}, ((_model_fields_fieldId2 = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId2.customType) ? {
|
|
49
|
+
customType: String((_model_fields_fieldId3 = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId3.customType)
|
|
50
|
+
} : {});
|
|
48
51
|
}),
|
|
49
52
|
visibleTo: Array.isArray(data['visibleTo']) ? data['visibleTo'] : [],
|
|
50
53
|
installedFrom: installedFrom(data['source'])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/dataset-record.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 type { PluginIndexedRecord } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { type DatasetModel, effectiveDatasetModel } from './dataset-models'\nimport { datasetDisplayName } from './datasets'\n\n/**\n * One stored dataset as this plugin shares it with another (AGL-3080) — the\n * one shape its server index (`dataset-record-index.ts`) and its console list\n * source (`dataset-record-list.ts`) both answer, so a reader on either side\n * reads the same record.\n *\n * `null` for a deleted dataset. Otherwise its id, its name as the Data page\n * shows it, and `facts`:\n * `fields` — its fields in the dataset page's order, each\n * `{ id, name, type }`; `visibleTo` — the scope tokens it is shared\n * with, so a reader acting for a member can apply the platform's own\n * visibility rule to them; and `installedFrom` — `{ listingId, version }`\n * for a dataset installed from a listing (`version` as the install\n * stamped it), `null` for one the workspace made. Never a record of it.\n */\nexport function datasetIndexedRecord(\n id: string,\n data: Readonly<Record<string, unknown>> | undefined,\n): PluginIndexedRecord | null {\n if (!data || data['deletedAt'] != null) return null\n const model = effectiveDatasetModel(data as { model?: DatasetModel; fields?: string[] })\n const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n return {\n id,\n name: datasetDisplayName(data) || id,\n facts: {\n fields: model.order.map((fieldId) => ({\n id: fieldId,\n name: text(model.fields[fieldId]?.name) || fieldId,\n type: String(model.fields[fieldId]?.type ?? 'text'),\n })),\n visibleTo: Array.isArray(data['visibleTo']) ? (data['visibleTo'] as string[]) : [],\n installedFrom: installedFrom(data['source']),\n },\n }\n}\n\n/**\n * Where an installed dataset came from. Read from its `source` stamp, which\n * every dataset install has written — the ones from before install\n * provenance (AGL-1015) included, which carry no `installedFrom`.\n */\nfunction installedFrom(source: unknown): { listingId: string; version: unknown } | null {\n const stamp = (source ?? {}) as Record<string, unknown>\n const listingId = typeof stamp['listingId'] === 'string' ? stamp['listingId'] : ''\n return listingId ? { listingId, version: stamp['version'] ?? null } : null\n}\n"],"names":["effectiveDatasetModel","datasetDisplayName","datasetIndexedRecord","id","data","model","text","value","trim","name","facts","fields","order","map","fieldId","type","String","visibleTo","Array","isArray","installedFrom","source","stamp","listingId","version"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/dataset-record.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 type { PluginIndexedRecord } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { type DatasetModel, effectiveDatasetModel } from './dataset-models'\nimport { datasetDisplayName } from './datasets'\n\n/**\n * One stored dataset as this plugin shares it with another (AGL-3080) — the\n * one shape its server index (`dataset-record-index.ts`) and its console list\n * source (`dataset-record-list.ts`) both answer, so a reader on either side\n * reads the same record.\n *\n * `null` for a deleted dataset. Otherwise its id, its name as the Data page\n * shows it, and `facts`:\n * `fields` — its fields in the dataset page's order, each\n * `{ id, name, type }`; `visibleTo` — the scope tokens it is shared\n * with, so a reader acting for a member can apply the platform's own\n * visibility rule to them; and `installedFrom` — `{ listingId, version }`\n * for a dataset installed from a listing (`version` as the install\n * stamped it), `null` for one the workspace made. Never a record of it.\n */\nexport function datasetIndexedRecord(\n id: string,\n data: Readonly<Record<string, unknown>> | undefined,\n): PluginIndexedRecord | null {\n if (!data || data['deletedAt'] != null) return null\n const model = effectiveDatasetModel(data as { model?: DatasetModel; fields?: string[] })\n const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n return {\n id,\n name: datasetDisplayName(data) || id,\n facts: {\n fields: model.order.map((fieldId) => ({\n id: fieldId,\n name: text(model.fields[fieldId]?.name) || fieldId,\n type: String(model.fields[fieldId]?.type ?? 'text'),\n // A named type riding the storage one: a record's photo is `image` (AGL-3616).\n ...(model.fields[fieldId]?.customType ? { customType: String(model.fields[fieldId]?.customType) } : {}),\n })),\n visibleTo: Array.isArray(data['visibleTo']) ? (data['visibleTo'] as string[]) : [],\n installedFrom: installedFrom(data['source']),\n },\n }\n}\n\n/**\n * Where an installed dataset came from. Read from its `source` stamp, which\n * every dataset install has written — the ones from before install\n * provenance (AGL-1015) included, which carry no `installedFrom`.\n */\nfunction installedFrom(source: unknown): { listingId: string; version: unknown } | null {\n const stamp = (source ?? {}) as Record<string, unknown>\n const listingId = typeof stamp['listingId'] === 'string' ? stamp['listingId'] : ''\n return listingId ? { listingId, version: stamp['version'] ?? null } : null\n}\n"],"names":["effectiveDatasetModel","datasetDisplayName","datasetIndexedRecord","id","data","model","text","value","trim","name","facts","fields","order","map","fieldId","type","String","customType","visibleTo","Array","isArray","installedFrom","source","stamp","listingId","version"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAA4BA,qBAAqB,QAAQ,sBAAkB;AAC3E,SAASC,kBAAkB,QAAQ,gBAAY;AAE/C;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,qBACdC,EAAU,EACVC,IAAmD;IAEnD,IAAI,CAACA,QAAQA,IAAI,CAAC,YAAY,IAAI,MAAM,OAAO;IAC/C,MAAMC,QAAQL,sBAAsBI;IACpC,MAAME,OAAO,CAACC,QAA4B,OAAOA,UAAU,WAAWA,MAAMC,IAAI,KAAK;IACrF,OAAO;QACLL;QACAM,MAAMR,mBAAmBG,SAASD;QAClCO,OAAO;YACLC,QAAQN,MAAMO,KAAK,CAACC,GAAG,CAAC,CAACC;;oBAEZT,uBACEA,wBAETA,wBAAyDA;uBALzB;oBACpCF,IAAIW;oBACJL,MAAMH,MAAKD,wBAAAA,MAAMM,MAAM,CAACG,QAAQ,qBAArBT,sBAAuBI,IAAI,KAAKK;oBAC3CC,MAAMC,gBAAOX,yBAAAA,MAAMM,MAAM,CAACG,QAAQ,qBAArBT,uBAAuBU,IAAI,mBAAI;mBAExCV,EAAAA,yBAAAA,MAAMM,MAAM,CAACG,QAAQ,qBAArBT,uBAAuBY,UAAU,IAAG;oBAAEA,YAAYD,QAAOX,yBAAAA,MAAMM,MAAM,CAACG,QAAQ,qBAArBT,uBAAuBY,UAAU;gBAAE,IAAI,CAAC;;YAEvGC,WAAWC,MAAMC,OAAO,CAAChB,IAAI,CAAC,YAAY,IAAKA,IAAI,CAAC,YAAY,GAAgB,EAAE;YAClFiB,eAAeA,cAAcjB,IAAI,CAAC,SAAS;QAC7C;IACF;AACF;AAEA;;;;CAIC,GACD,SAASiB,cAAcC,MAAe;QAGKC;IAFzC,MAAMA,QAASD,iBAAAA,SAAU,CAAC;IAC1B,MAAME,YAAY,OAAOD,KAAK,CAAC,YAAY,KAAK,WAAWA,KAAK,CAAC,YAAY,GAAG;IAChF,OAAOC,YAAY;QAAEA;QAAWC,OAAO,GAAEF,iBAAAA,KAAK,CAAC,UAAU,YAAhBA,iBAAoB;IAAK,IAAI;AACxE"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -24,6 +24,7 @@ import { registerPageRecordSource } from "@aglyn/aglyn/app-utils/page-record-sou
|
|
|
24
24
|
import { registerPluginTransferResourceUi } from "@aglyn/aglyn/plugin-manager/plugin-transfer-resources";
|
|
25
25
|
import { DATASET_TRANSFER_RESOURCE } from "./transfer/dataset-transfer-key.js";
|
|
26
26
|
import { DATASET_REPEAT_SOURCE } from "./repeat/dataset-repeat-source.js";
|
|
27
|
+
import { DATASET_IMAGE_FIELD } from "./model/dataset-image-field.js";
|
|
27
28
|
import { RECORD_PAGE_ADDRESS_FIELD } from "./record-pages/record-pages.js";
|
|
28
29
|
import { RECORD_PAGE_SOURCE } from "./record-pages/record-page-source.js";
|
|
29
30
|
import { registerDataRecordRoutes } from "./model/data-record-routes.js";
|
|
@@ -54,6 +55,8 @@ import RecordTemplateSection from "./record-pages/record-template-section.compon
|
|
|
54
55
|
// The "Page address" field type (AGL-3475), for the schema dialog's type
|
|
55
56
|
// list and the record editor's checks.
|
|
56
57
|
Aglyn.registerCustomFieldType(RECORD_PAGE_ADDRESS_FIELD);
|
|
58
|
+
// A record's photo (AGL-3616), which a page's Image binds.
|
|
59
|
+
Aglyn.registerCustomFieldType(DATASET_IMAGE_FIELD);
|
|
57
60
|
// A record template draws on the canvas for one of its records (AGL-3475).
|
|
58
61
|
registerPageRecordSource(RECORD_PAGE_SOURCE);
|
|
59
62
|
// The workspace's datasets, for another plugin's picker (AGL-3080).
|
package/src/lib/plugin.js.map
CHANGED
|
@@ -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 { 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,
|
|
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 { DATASET_IMAGE_FIELD } from './model/dataset-image-field'\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's photo (AGL-3616), which a page's Image binds.\n Aglyn.registerCustomFieldType(DATASET_IMAGE_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","DATASET_IMAGE_FIELD","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,mBAAmB,QAAQ,iCAA6B;AACjE,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,kBAAkBb,KAAK,IAAM,MAAM,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASc;IACdhB,qBAAqBQ;IACrB,yEAAyE;IACzE,uCAAuC;IACvCT,MAAMkB,uBAAuB,CAACP;IAC9B,2DAA2D;IAC3DX,MAAMkB,uBAAuB,CAACR;IAC9B,2EAA2E;IAC3EJ,yBAAyBM;IACzB,oEAAoE;IACpEP;IACA,wEAAwE;IACxE,wCAAwC;IACxCQ;IACA,iEAAiE;IACjE,kEAAkE;IAClEN,iCACEC,2BACA,wFAAwF;IACxF;QAAEW,OAAO;QAAmBC,MAAM;YAAEC,MAAMnB,mBAAmBmB,IAAI;QAAC;QAAGC,eAAe;IAAU,GAC9F;QAAEC,UAAUnB;IAAU;IAExBJ,MAAMwB,wBAAwB,CAAC;QAC7B,+DAA+D;QAC/D,yBAAyB;QACzBC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,WAAW7B;YACb;YACA,uEAAuE;YACvE,kCAAkC;YAClC;gBACE2B,MAAM;gBACNC,UAAU;gBACVC,WAAWb;YACb;YACA;gBACEW,MAAM;gBACNC,UAAU;gBACVC,WAAWd;YACb;SACD;QACDS,UAAUnB;QACVyB,aAAa;QACbC,aAAa;QACbC,UAAU;YACR;gBACEZ,OAAO;gBACPa,MAAM;gBACN,qEAAqE;gBACrE,8DAA8D;gBAC9DC,UAAU;gBACVb,MAAM;oBAAEC,MAAMnB,mBAAmBmB,IAAI;gBAAC;gBACtCa,QAAQ;oBACNC,OAAO;oBACPf,MAAM;wBAAEC,MAAMnB,mBAAmBmB,IAAI;oBAAC;oBACtCe,WAAW;gBACb;gBACAR,WAAWZ;YACb;SACD;IACH;AACF;AAEA,SAASqB,WAAWtC,gBAAgB,QAAQ,+CAA2C"}
|
|
@@ -43,6 +43,10 @@ import { type DatasetFieldType, type DatasetModel } from '../model/dataset-model
|
|
|
43
43
|
* the route's own words.
|
|
44
44
|
* - THE CHECK holds every record to the model it is written under
|
|
45
45
|
* (`validateDocument`), so what is seeded is what a person could type.
|
|
46
|
+
* - A PHOTO is an `image` field (AGL-3616): text storage riding the
|
|
47
|
+
* "Image" custom type, each value an address the site's Image element
|
|
48
|
+
* shows (`isDatasetImageValue`), so a portfolio's pieces or a team keep
|
|
49
|
+
* the photo the page that lists them binds.
|
|
46
50
|
*
|
|
47
51
|
* Nothing is published. The writer touches the one new dataset and its
|
|
48
52
|
* records; a page shows them only where a person (or a job's draft page)
|
|
@@ -66,7 +70,8 @@ export declare const DATASET_DRAFT_LIST_MAX = 12;
|
|
|
66
70
|
* The field types a caller names, in words a person reads, and the type
|
|
67
71
|
* each one is stored as. A date or a time is written as text the way a
|
|
68
72
|
* visitor reads it ("Saturday 8 November, 7 pm"): a stored timestamp renders
|
|
69
|
-
* as a number in a repeat's `{{item.<field>}}`.
|
|
73
|
+
* as a number in a repeat's `{{item.<field>}}`. An image is text holding a
|
|
74
|
+
* photo's address, under the "Image" custom type (AGL-3616).
|
|
70
75
|
*/
|
|
71
76
|
export declare const DATASET_DRAFT_FIELD_TYPES: {
|
|
72
77
|
readonly text: "text";
|
|
@@ -74,6 +79,7 @@ export declare const DATASET_DRAFT_FIELD_TYPES: {
|
|
|
74
79
|
readonly integer: "int32";
|
|
75
80
|
readonly boolean: "bool";
|
|
76
81
|
readonly list: "sorted";
|
|
82
|
+
readonly image: "text";
|
|
77
83
|
};
|
|
78
84
|
export type DatasetDraftFieldType = keyof typeof DATASET_DRAFT_FIELD_TYPES;
|
|
79
85
|
/** The datasets route's refusals, word for word. */
|
|
@@ -120,6 +126,8 @@ export interface DatasetDraftFacts {
|
|
|
120
126
|
records: number;
|
|
121
127
|
/** The page-address field's id, where the dataset has one. */
|
|
122
128
|
addressField: string | null;
|
|
129
|
+
/** The first photo field's id, where the dataset has one (AGL-3616). */
|
|
130
|
+
imageField: string | null;
|
|
123
131
|
}
|
|
124
132
|
/** The draft as the writer will store it. */
|
|
125
133
|
export interface DatasetDraftRead {
|
|
@@ -25,6 +25,7 @@ import { memberHasOrgPermission, resolveOrgIdForHost, resolveOrgMembership } fro
|
|
|
25
25
|
import { isServerReleaseFlagOnForOrg } from "@aglyn/tenant-data-admin/server/release-flags";
|
|
26
26
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
27
27
|
import { coerceDocumentValues, datasetIntegrityFields, defaultDatasetFieldId, validateDocument } from "../model/dataset-models.js";
|
|
28
|
+
import { DATASET_IMAGE_FIELD_TYPE, isDatasetImageValue } from "../model/dataset-image-field.js";
|
|
28
29
|
import { RECORD_PAGE_ADDRESS_FIELD_TYPE, RECORD_PAGE_ADDRESS_MAX, uniqueRecordAddresses } from "../record-pages/record-pages.js";
|
|
29
30
|
/** The resource name this writer is registered under. */ export const DATASET_DRAFT_RESOURCE = 'dataset';
|
|
30
31
|
/** The longest dataset name, as the datasets route keeps it. */ export const DATASET_DRAFT_NAME_MAX = 120;
|
|
@@ -37,13 +38,15 @@ export const DATASET_DRAFT_FIELD_NAME_MAX = 60;
|
|
|
37
38
|
* The field types a caller names, in words a person reads, and the type
|
|
38
39
|
* each one is stored as. A date or a time is written as text the way a
|
|
39
40
|
* visitor reads it ("Saturday 8 November, 7 pm"): a stored timestamp renders
|
|
40
|
-
* as a number in a repeat's `{{item.<field>}}`.
|
|
41
|
+
* as a number in a repeat's `{{item.<field>}}`. An image is text holding a
|
|
42
|
+
* photo's address, under the "Image" custom type (AGL-3616).
|
|
41
43
|
*/ export const DATASET_DRAFT_FIELD_TYPES = {
|
|
42
44
|
text: 'text',
|
|
43
45
|
number: 'float',
|
|
44
46
|
integer: 'int32',
|
|
45
47
|
boolean: 'bool',
|
|
46
|
-
list: 'sorted'
|
|
48
|
+
list: 'sorted',
|
|
49
|
+
image: 'text'
|
|
47
50
|
};
|
|
48
51
|
const DRAFT_TYPES = Object.keys(DATASET_DRAFT_FIELD_TYPES);
|
|
49
52
|
/** The datasets route's refusals, word for word. */ export const DATASET_DRAFT_ROLE_REFUSAL = 'Editing org data requires the editor role';
|
|
@@ -118,7 +121,9 @@ function isRecord(value) {
|
|
|
118
121
|
model.fields[id] = _extends({
|
|
119
122
|
name: fieldName,
|
|
120
123
|
type: DATASET_DRAFT_FIELD_TYPES[rawType]
|
|
121
|
-
},
|
|
124
|
+
}, rawType === 'image' ? {
|
|
125
|
+
customType: DATASET_IMAGE_FIELD_TYPE
|
|
126
|
+
} : {}, isRecord(raw) && raw['required'] === true ? {
|
|
122
127
|
required: true
|
|
123
128
|
} : {});
|
|
124
129
|
model.order.push(id);
|
|
@@ -129,9 +134,9 @@ function isRecord(value) {
|
|
|
129
134
|
let addressField = null;
|
|
130
135
|
const rawAddress = content['pageAddressFrom'];
|
|
131
136
|
if (rawAddress !== undefined && rawAddress !== null && rawAddress !== '') {
|
|
132
|
-
var _model_fields_sourceId;
|
|
137
|
+
var _model_fields_sourceId, _model_fields_sourceId1;
|
|
133
138
|
const sourceId = byName.get(line(rawAddress).toLowerCase());
|
|
134
|
-
if (!sourceId || ((_model_fields_sourceId = model.fields[sourceId]) == null ? void 0 : _model_fields_sourceId.type) !== 'text') {
|
|
139
|
+
if (!sourceId || ((_model_fields_sourceId = model.fields[sourceId]) == null ? void 0 : _model_fields_sourceId.type) !== 'text' || ((_model_fields_sourceId1 = model.fields[sourceId]) == null ? void 0 : _model_fields_sourceId1.customType)) {
|
|
135
140
|
problems.push('A page address fills from one of the dataset’s text fields');
|
|
136
141
|
} else {
|
|
137
142
|
addressField = defaultDatasetFieldId('slug', new Set(model.order));
|
|
@@ -173,6 +178,14 @@ function isRecord(value) {
|
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
const errors = validateDocument(model, values);
|
|
181
|
+
// A photo is an address the site's Image shows, whether or not the
|
|
182
|
+
// "Image" type's check is registered in this process (AGL-3616).
|
|
183
|
+
for (const [id, value] of Object.entries(values)){
|
|
184
|
+
const field = model.fields[id];
|
|
185
|
+
if ((field == null ? void 0 : field.customType) === DATASET_IMAGE_FIELD_TYPE && !errors[id] && !isDatasetImageValue(value)) {
|
|
186
|
+
errors[id] = `${field.name} is not a photo from the media library or an https link`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
176
189
|
for (const error of Object.values(errors))problems.push(`Record ${index + 1}: ${error}`);
|
|
177
190
|
if (!Object.keys(values).length) problems.push(`Record ${index + 1} has no values`);
|
|
178
191
|
records.push(values);
|
|
@@ -207,6 +220,13 @@ function isRecord(value) {
|
|
|
207
220
|
}
|
|
208
221
|
};
|
|
209
222
|
}
|
|
223
|
+
/** The first photo field of a model, by id. */ function imageFieldOf(model) {
|
|
224
|
+
var _model_order_find;
|
|
225
|
+
return (_model_order_find = model.order.find((id)=>{
|
|
226
|
+
var _model_fields_id;
|
|
227
|
+
return ((_model_fields_id = model.fields[id]) == null ? void 0 : _model_fields_id.customType) === DATASET_IMAGE_FIELD_TYPE;
|
|
228
|
+
})) != null ? _model_order_find : null;
|
|
229
|
+
}
|
|
210
230
|
function factsOf(model, records, addressField) {
|
|
211
231
|
return {
|
|
212
232
|
fields: model.order.map((id)=>{
|
|
@@ -219,7 +239,8 @@ function factsOf(model, records, addressField) {
|
|
|
219
239
|
};
|
|
220
240
|
}),
|
|
221
241
|
records,
|
|
222
|
-
addressField
|
|
242
|
+
addressField,
|
|
243
|
+
imageField: imageFieldOf(model)
|
|
223
244
|
};
|
|
224
245
|
}
|
|
225
246
|
/** Whether content is a dataset this plugin would store, with what it says about it. Pure. */ export function checkDatasetDraftContent(content) {
|
|
@@ -1 +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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"}
|
|
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 { DATASET_IMAGE_FIELD_TYPE, isDatasetImageValue } from '../model/dataset-image-field'\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 * - A PHOTO is an `image` field (AGL-3616): text storage riding the\n * \"Image\" custom type, each value an address the site's Image element\n * shows (`isDatasetImageValue`), so a portfolio's pieces or a team keep\n * the photo the page that lists them binds.\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>}}`. An image is text holding a\n * photo's address, under the \"Image\" custom type (AGL-3616).\n */\nexport const DATASET_DRAFT_FIELD_TYPES = {\n text: 'text',\n number: 'float',\n integer: 'int32',\n boolean: 'bool',\n list: 'sorted',\n image: 'text',\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 /** The first photo field's id, where the dataset has one (AGL-3616). */\n imageField: 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 ...(rawType === 'image' ? { customType: DATASET_IMAGE_FIELD_TYPE } : {}),\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' || model.fields[sourceId]?.customType) {\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 // A photo is an address the site's Image shows, whether or not the\n // \"Image\" type's check is registered in this process (AGL-3616).\n for (const [id, value] of Object.entries(values)) {\n const field = model.fields[id]\n if (field?.customType === DATASET_IMAGE_FIELD_TYPE && !errors[id] && !isDatasetImageValue(value)) {\n errors[id] = `${field.name} is not a photo from the media library or an https link`\n }\n }\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\n/** The first photo field of a model, by id. */\nfunction imageFieldOf(model: DatasetModel): string | null {\n return model.order.find((id) => model.fields[id]?.customType === DATASET_IMAGE_FIELD_TYPE) ?? null\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 imageField: imageFieldOf(model),\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","DATASET_IMAGE_FIELD_TYPE","isDatasetImageValue","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","image","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","customType","required","set","addressField","rawAddress","sourceId","get","slugFrom","rawRecords","records","forEach","index","input","key","entries","values","errors","field","error","ok","source","assigned","String","address","imageFieldOf","find","factsOf","imageField","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","datasetDraftWriter","registerDatasetDraftWriter","pluginId"],"mappings":"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"}
|