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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/package.json +11 -9
  2. package/src/lib/constants/bundle-common.d.ts +6 -0
  3. package/src/lib/constants/bundle-common.js +5 -0
  4. package/src/lib/constants/bundle-common.js.map +1 -1
  5. package/src/lib/declarations.server.d.ts +39 -0
  6. package/src/lib/declarations.server.js +63 -0
  7. package/src/lib/declarations.server.js.map +1 -0
  8. package/src/lib/form-target/dataset-form-record-target.server.d.ts +40 -0
  9. package/src/lib/form-target/dataset-form-record-target.server.js +160 -0
  10. package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -0
  11. package/src/lib/form-target/form-dataset-binding-token.d.ts +35 -0
  12. package/src/lib/form-target/form-dataset-binding-token.js +107 -0
  13. package/src/lib/form-target/form-dataset-binding-token.js.map +1 -0
  14. package/src/lib/form-target/form-dataset-binding.d.ts +50 -0
  15. package/src/lib/form-target/form-dataset-binding.js +74 -0
  16. package/src/lib/form-target/form-dataset-binding.js.map +1 -0
  17. package/src/lib/form-target/stamp-form-dataset-bindings.d.ts +39 -0
  18. package/src/lib/form-target/stamp-form-dataset-bindings.js +65 -0
  19. package/src/lib/form-target/stamp-form-dataset-bindings.js.map +1 -0
  20. package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -2
  21. package/src/lib/repeat/dataset-repeat-rows.js +2 -2
  22. package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
  23. package/src/lib/repeat/dataset-repeat-rows.server.d.ts +43 -0
  24. package/src/lib/repeat/dataset-repeat-rows.server.js +170 -0
  25. package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -0
  26. package/src/lib/repeat/dataset-repeat-source.js +3 -1
  27. package/src/lib/repeat/dataset-repeat-source.js.map +1 -1
  28. package/src/lib/site-bundle/datasets-site-bundle.server.d.ts +87 -0
  29. package/src/lib/site-bundle/datasets-site-bundle.server.js +197 -0
  30. package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -0
@@ -0,0 +1,74 @@
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 { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { collectFormFieldNodeIds, FORM_COMPONENT_ID } from "@aglyn/aglyn/app-utils/forms";
18
+ /** The most fields a submission may carry, and so the most a map can name. */ export const FORM_DATASET_BINDING_MAX_FIELDS = 20;
19
+ const text = (value, max)=>typeof value === 'string' ? value.trim().slice(0, max) : '';
20
+ /**
21
+ * The dataset binding a `form` node declares, or null when it declares none.
22
+ *
23
+ * The map is built the way the browser builds the submission: every
24
+ * `formField` under the form, in reading order, keyed by the name the field
25
+ * submits under (`fieldName`, or `field` when it has none), and a later field
26
+ * under the same name replaces an earlier one. A field with no
27
+ * `datasetFieldId` maps nothing and its value falls back to matching dataset
28
+ * fields by name.
29
+ *
30
+ * Read from the COMPOSED tree, so a form grafted from a layout, a reusable
31
+ * component or a form entity is read exactly as the page renders it.
32
+ */ export function formDatasetBindingOf(nodes, formNodeId) {
33
+ var _node_props;
34
+ const node = nodes == null ? void 0 : nodes[formNodeId];
35
+ if ((node == null ? void 0 : node.componentId) !== FORM_COMPONENT_ID) return null;
36
+ const props = (_node_props = node.props) != null ? _node_props : {};
37
+ const datasetId = text(props['datasetId'], 128);
38
+ const datasetName = text(props['datasetName'], 60);
39
+ if (!datasetId && !datasetName) return null;
40
+ const fieldMap = {};
41
+ for (const fieldNodeId of collectFormFieldNodeIds(nodes, formNodeId)){
42
+ var _ref;
43
+ var _nodes_fieldNodeId;
44
+ const fieldProps = (_ref = nodes == null ? void 0 : (_nodes_fieldNodeId = nodes[fieldNodeId]) == null ? void 0 : _nodes_fieldNodeId.props) != null ? _ref : {};
45
+ const datasetFieldId = text(fieldProps['datasetFieldId'], 64);
46
+ if (!datasetFieldId) continue;
47
+ const submittedKey = text(fieldProps['fieldName'], 64) || 'field';
48
+ if (!(submittedKey in fieldMap) && Object.keys(fieldMap).length >= FORM_DATASET_BINDING_MAX_FIELDS) {
49
+ continue;
50
+ }
51
+ fieldMap[submittedKey] = datasetFieldId;
52
+ }
53
+ return _extends({}, datasetId ? {
54
+ datasetId
55
+ } : {}, datasetName ? {
56
+ datasetName
57
+ } : {}, {
58
+ fieldMap
59
+ });
60
+ }
61
+ /** Every `form` node in a composed tree that declares a dataset binding. */ export function formDatasetBindingsOf(nodes) {
62
+ const found = [];
63
+ for (const [nodeId, node] of Object.entries(nodes != null ? nodes : {})){
64
+ if ((node == null ? void 0 : node.componentId) !== FORM_COMPONENT_ID) continue;
65
+ const binding = formDatasetBindingOf(nodes, nodeId);
66
+ if (binding) found.push({
67
+ nodeId: nodeId,
68
+ binding
69
+ });
70
+ }
71
+ return found;
72
+ }
73
+
74
+ //# sourceMappingURL=form-dataset-binding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/form-target/form-dataset-binding.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 { AglynNodeSchema, NodeId } from '@aglyn/aglyn'\nimport {\n collectFormFieldNodeIds,\n FORM_COMPONENT_ID,\n} from '@aglyn/aglyn/app-utils/forms'\n\n/**\n * Where a form's submissions are also written as dataset records, read off\n * the form as a page renders it.\n */\nexport interface FormDatasetBinding {\n /** The dataset, by id, so renaming it never breaks the binding (AGL-556). */\n datasetId?: string\n /** The legacy by-name binding, honored when no id is set (AGL-141). */\n datasetName?: string\n /** Submitted field name → the dataset field id the value is stored under. */\n fieldMap: Record<string, string>\n}\n\n/** The most fields a submission may carry, and so the most a map can name. */\nexport const FORM_DATASET_BINDING_MAX_FIELDS = 20\n\nconst text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\n/**\n * The dataset binding a `form` node declares, or null when it declares none.\n *\n * The map is built the way the browser builds the submission: every\n * `formField` under the form, in reading order, keyed by the name the field\n * submits under (`fieldName`, or `field` when it has none), and a later field\n * under the same name replaces an earlier one. A field with no\n * `datasetFieldId` maps nothing and its value falls back to matching dataset\n * fields by name.\n *\n * Read from the COMPOSED tree, so a form grafted from a layout, a reusable\n * component or a form entity is read exactly as the page renders it.\n */\nexport function formDatasetBindingOf(\n nodes: Record<NodeId, AglynNodeSchema | undefined> | undefined | null,\n formNodeId: NodeId,\n): FormDatasetBinding | null {\n const node = nodes?.[formNodeId]\n if (node?.componentId !== FORM_COMPONENT_ID) return null\n const props = (node.props ?? {}) as Record<string, unknown>\n const datasetId = text(props['datasetId'], 128)\n const datasetName = text(props['datasetName'], 60)\n if (!datasetId && !datasetName) return null\n const fieldMap: Record<string, string> = {}\n for (const fieldNodeId of collectFormFieldNodeIds(nodes, formNodeId)) {\n const fieldProps = (nodes?.[fieldNodeId]?.props ?? {}) as Record<\n string,\n unknown\n >\n const datasetFieldId = text(fieldProps['datasetFieldId'], 64)\n if (!datasetFieldId) continue\n const submittedKey = text(fieldProps['fieldName'], 64) || 'field'\n if (\n !(submittedKey in fieldMap) &&\n Object.keys(fieldMap).length >= FORM_DATASET_BINDING_MAX_FIELDS\n ) {\n continue\n }\n fieldMap[submittedKey] = datasetFieldId\n }\n return {\n ...(datasetId ? { datasetId } : {}),\n ...(datasetName ? { datasetName } : {}),\n fieldMap,\n }\n}\n\n/** Every `form` node in a composed tree that declares a dataset binding. */\nexport function formDatasetBindingsOf(\n nodes: Record<NodeId, AglynNodeSchema | undefined> | undefined | null,\n): Array<{ nodeId: NodeId; binding: FormDatasetBinding }> {\n const found: Array<{ nodeId: NodeId; binding: FormDatasetBinding }> = []\n for (const [nodeId, node] of Object.entries(nodes ?? {})) {\n if (node?.componentId !== FORM_COMPONENT_ID) continue\n const binding = formDatasetBindingOf(nodes, nodeId as NodeId)\n if (binding) found.push({ nodeId: nodeId as NodeId, binding })\n }\n return found\n}\n"],"names":["collectFormFieldNodeIds","FORM_COMPONENT_ID","FORM_DATASET_BINDING_MAX_FIELDS","text","value","max","trim","slice","formDatasetBindingOf","nodes","formNodeId","node","componentId","props","datasetId","datasetName","fieldMap","fieldNodeId","fieldProps","datasetFieldId","submittedKey","Object","keys","length","formDatasetBindingsOf","found","nodeId","entries","binding","push"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,uBAAuB,EACvBC,iBAAiB,QACZ,+BAA8B;AAerC,4EAA4E,GAC5E,OAAO,MAAMC,kCAAkC,GAAE;AAEjD,MAAMC,OAAO,CAACC,OAAgBC,MAC5B,OAAOD,UAAU,WAAWA,MAAME,IAAI,GAAGC,KAAK,CAAC,GAAGF,OAAO;AAE3D;;;;;;;;;;;;CAYC,GACD,OAAO,SAASG,qBACdC,KAAqE,EACrEC,UAAkB;QAIHC;IAFf,MAAMA,OAAOF,yBAAAA,KAAO,CAACC,WAAW;IAChC,IAAIC,CAAAA,wBAAAA,KAAMC,WAAW,MAAKX,mBAAmB,OAAO;IACpD,MAAMY,SAASF,cAAAA,KAAKE,KAAK,YAAVF,cAAc,CAAC;IAC9B,MAAMG,YAAYX,KAAKU,KAAK,CAAC,YAAY,EAAE;IAC3C,MAAME,cAAcZ,KAAKU,KAAK,CAAC,cAAc,EAAE;IAC/C,IAAI,CAACC,aAAa,CAACC,aAAa,OAAO;IACvC,MAAMC,WAAmC,CAAC;IAC1C,KAAK,MAAMC,eAAejB,wBAAwBS,OAAOC,YAAa;;YAChDD;QAApB,MAAMS,qBAAcT,0BAAAA,qBAAAA,KAAO,CAACQ,YAAY,qBAApBR,mBAAsBI,KAAK,mBAAI,CAAC;QAIpD,MAAMM,iBAAiBhB,KAAKe,UAAU,CAAC,iBAAiB,EAAE;QAC1D,IAAI,CAACC,gBAAgB;QACrB,MAAMC,eAAejB,KAAKe,UAAU,CAAC,YAAY,EAAE,OAAO;QAC1D,IACE,CAAEE,CAAAA,gBAAgBJ,QAAO,KACzBK,OAAOC,IAAI,CAACN,UAAUO,MAAM,IAAIrB,iCAChC;YACA;QACF;QACAc,QAAQ,CAACI,aAAa,GAAGD;IAC3B;IACA,OAAO,aACDL,YAAY;QAAEA;IAAU,IAAI,CAAC,GAC7BC,cAAc;QAAEA;IAAY,IAAI,CAAC;QACrCC;;AAEJ;AAEA,0EAA0E,GAC1E,OAAO,SAASQ,sBACdf,KAAqE;IAErE,MAAMgB,QAAgE,EAAE;IACxE,KAAK,MAAM,CAACC,QAAQf,KAAK,IAAIU,OAAOM,OAAO,CAAClB,gBAAAA,QAAS,CAAC,GAAI;QACxD,IAAIE,CAAAA,wBAAAA,KAAMC,WAAW,MAAKX,mBAAmB;QAC7C,MAAM2B,UAAUpB,qBAAqBC,OAAOiB;QAC5C,IAAIE,SAASH,MAAMI,IAAI,CAAC;YAAEH,QAAQA;YAAkBE;QAAQ;IAC9D;IACA,OAAOH;AACT"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * The prop a rendered `form` node carries its signed dataset binding in. The
19
+ * form element posts it back as {@link FORM_DATASET_BINDING_FIELD}. Both are
20
+ * on pages already rendered and cached — never rename.
21
+ */
22
+ export declare const FORM_DATASET_BINDING_PROP = "datasetBindingToken";
23
+ /** The submission body field the form element posts the signed binding in. */
24
+ export declare const FORM_DATASET_BINDING_FIELD = "datasetBinding";
25
+ /**
26
+ * Signs every `form` node's dataset binding into its props, for this plugin's
27
+ * form record target to verify when the submission comes back (AGL-2773) —
28
+ * the stamping half of `plugin-manager/submission-record-target.ts`.
29
+ *
30
+ * A record is written only where a valid signature says, so a form
31
+ * without one still submits — the Inbox copy is always written — but adds no
32
+ * record. That is what happens when `TOKEN_SIGNING_SECRET` is missing: the
33
+ * tree is returned unsigned rather than failing the page render, and the
34
+ * missing secret is reported once per process instead of once per render.
35
+ *
36
+ * Never mutates `nodes`; a tree with no bound form comes back as the same
37
+ * object.
38
+ */
39
+ export declare function stampFormDatasetBindings<N extends Record<string, any>>(nodes: N, hostId: string): N;
@@ -0,0 +1,65 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { formDatasetBindingsOf } from "./form-dataset-binding.js";
18
+ import { signFormDatasetBinding } from "./form-dataset-binding-token.js";
19
+ /**
20
+ * The prop a rendered `form` node carries its signed dataset binding in. The
21
+ * form element posts it back as {@link FORM_DATASET_BINDING_FIELD}. Both are
22
+ * on pages already rendered and cached — never rename.
23
+ */ export const FORM_DATASET_BINDING_PROP = 'datasetBindingToken';
24
+ /** The submission body field the form element posts the signed binding in. */ export const FORM_DATASET_BINDING_FIELD = 'datasetBinding';
25
+ /** Whether an unsigned render has been reported in this process yet. */ let reportedUnsigned = false;
26
+ /**
27
+ * Signs every `form` node's dataset binding into its props, for this plugin's
28
+ * form record target to verify when the submission comes back (AGL-2773) —
29
+ * the stamping half of `plugin-manager/submission-record-target.ts`.
30
+ *
31
+ * A record is written only where a valid signature says, so a form
32
+ * without one still submits — the Inbox copy is always written — but adds no
33
+ * record. That is what happens when `TOKEN_SIGNING_SECRET` is missing: the
34
+ * tree is returned unsigned rather than failing the page render, and the
35
+ * missing secret is reported once per process instead of once per render.
36
+ *
37
+ * Never mutates `nodes`; a tree with no bound form comes back as the same
38
+ * object.
39
+ */ export function stampFormDatasetBindings(nodes, hostId) {
40
+ const bindings = formDatasetBindingsOf(nodes);
41
+ if (!bindings.length) return nodes;
42
+ const signed = _extends({}, nodes);
43
+ for (const { nodeId, binding } of bindings){
44
+ var _ref;
45
+ let token;
46
+ try {
47
+ token = signFormDatasetBinding(hostId, binding);
48
+ } catch (error) {
49
+ if (!reportedUnsigned) {
50
+ reportedUnsigned = true;
51
+ console.error('Form dataset bindings are not being signed, so forms bound to a ' + 'dataset will not write records', error);
52
+ }
53
+ return nodes;
54
+ }
55
+ const node = signed[nodeId];
56
+ signed[nodeId] = _extends({}, node, {
57
+ props: _extends({}, (_ref = node == null ? void 0 : node.props) != null ? _ref : {}, {
58
+ [FORM_DATASET_BINDING_PROP]: token
59
+ })
60
+ });
61
+ }
62
+ return signed;
63
+ }
64
+
65
+ //# sourceMappingURL=stamp-form-dataset-bindings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/form-target/stamp-form-dataset-bindings.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 { formDatasetBindingsOf } from './form-dataset-binding'\nimport { signFormDatasetBinding } from './form-dataset-binding-token'\n\n/**\n * The prop a rendered `form` node carries its signed dataset binding in. The\n * form element posts it back as {@link FORM_DATASET_BINDING_FIELD}. Both are\n * on pages already rendered and cached — never rename.\n */\nexport const FORM_DATASET_BINDING_PROP = 'datasetBindingToken'\n\n/** The submission body field the form element posts the signed binding in. */\nexport const FORM_DATASET_BINDING_FIELD = 'datasetBinding'\n\n/** Whether an unsigned render has been reported in this process yet. */\nlet reportedUnsigned = false\n\n/**\n * Signs every `form` node's dataset binding into its props, for this plugin's\n * form record target to verify when the submission comes back (AGL-2773) —\n * the stamping half of `plugin-manager/submission-record-target.ts`.\n *\n * A record is written only where a valid signature says, so a form\n * without one still submits — the Inbox copy is always written — but adds no\n * record. That is what happens when `TOKEN_SIGNING_SECRET` is missing: the\n * tree is returned unsigned rather than failing the page render, and the\n * missing secret is reported once per process instead of once per render.\n *\n * Never mutates `nodes`; a tree with no bound form comes back as the same\n * object.\n */\nexport function stampFormDatasetBindings<N extends Record<string, any>>(\n nodes: N,\n hostId: string,\n): N {\n const bindings = formDatasetBindingsOf(nodes as never)\n if (!bindings.length) return nodes\n const signed: Record<string, any> = { ...nodes }\n for (const { nodeId, binding } of bindings) {\n let token: string\n try {\n token = signFormDatasetBinding(hostId, binding)\n } catch (error) {\n if (!reportedUnsigned) {\n reportedUnsigned = true\n console.error(\n 'Form dataset bindings are not being signed, so forms bound to a ' +\n 'dataset will not write records',\n error,\n )\n }\n return nodes\n }\n const node = signed[nodeId]\n signed[nodeId] = {\n ...node,\n props: { ...(node?.props ?? {}), [FORM_DATASET_BINDING_PROP]: token },\n }\n }\n return signed as N\n}\n"],"names":["formDatasetBindingsOf","signFormDatasetBinding","FORM_DATASET_BINDING_PROP","FORM_DATASET_BINDING_FIELD","reportedUnsigned","stampFormDatasetBindings","nodes","hostId","bindings","length","signed","nodeId","binding","token","error","console","node","props"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,qBAAqB,QAAQ,4BAAwB;AAC9D,SAASC,sBAAsB,QAAQ,kCAA8B;AAErE;;;;CAIC,GACD,OAAO,MAAMC,4BAA4B,sBAAqB;AAE9D,4EAA4E,GAC5E,OAAO,MAAMC,6BAA6B,iBAAgB;AAE1D,sEAAsE,GACtE,IAAIC,mBAAmB;AAEvB;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,yBACdC,KAAQ,EACRC,MAAc;IAEd,MAAMC,WAAWR,sBAAsBM;IACvC,IAAI,CAACE,SAASC,MAAM,EAAE,OAAOH;IAC7B,MAAMI,SAA8B,aAAKJ;IACzC,KAAK,MAAM,EAAEK,MAAM,EAAEC,OAAO,EAAE,IAAIJ,SAAU;;QAC1C,IAAIK;QACJ,IAAI;YACFA,QAAQZ,uBAAuBM,QAAQK;QACzC,EAAE,OAAOE,OAAO;YACd,IAAI,CAACV,kBAAkB;gBACrBA,mBAAmB;gBACnBW,QAAQD,KAAK,CACX,qEACE,kCACFA;YAEJ;YACA,OAAOR;QACT;QACA,MAAMU,OAAON,MAAM,CAACC,OAAO;QAC3BD,MAAM,CAACC,OAAO,GAAG,aACZK;YACHC,OAAO,qBAAMD,wBAAAA,KAAMC,KAAK,mBAAI,CAAC;gBAAI,CAACf,0BAA0B,EAAEW;;;IAElE;IACA,OAAOH;AACT"}
@@ -27,8 +27,8 @@ export interface DatasetRepeatRowsRequest {
27
27
  }
28
28
  /**
29
29
  * A dataset's rows for the besigner's canvas preview, read the way the
30
- * published page's composition reads them (`getDatasets` in the tenant
31
- * runtime), with the client SDK instead of the Admin one (AGL-3111).
30
+ * published page's composition reads them (`readPublishedDatasetRows`, beside
31
+ * this file), with the client SDK instead of the Admin one (AGL-3111).
32
32
  *
33
33
  * - The key resolves as the page resolves it: an id first, then a display
34
34
  * name within what this site may see. An id that exists but that this site
@@ -18,8 +18,8 @@ import { repeatRecordsFromPages } from "@aglyn/tenant-runtime/repeat-record-page
18
18
  import { collection, doc, documentId, getDoc, getDocs, limit, orderBy, query, where } from "firebase/firestore";
19
19
  /**
20
20
  * A dataset's rows for the besigner's canvas preview, read the way the
21
- * published page's composition reads them (`getDatasets` in the tenant
22
- * runtime), with the client SDK instead of the Admin one (AGL-3111).
21
+ * published page's composition reads them (`readPublishedDatasetRows`, beside
22
+ * this file), with the client SDK instead of the Admin one (AGL-3111).
23
23
  *
24
24
  * - The key resolves as the page resolves it: an id first, then a display
25
25
  * name within what this site may see. An id that exists but that this site
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/repeat/dataset-repeat-rows.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n datasetDisplayName,\n effectiveDatasetModel,\n type HostDataset,\n type HostDatasetRecord,\n REPEAT_MAX_RECORDS,\n type RepeatableDataset,\n scopeTokensForHost,\n visibleToHost,\n} from '@aglyn/aglyn'\nimport type { RepeatRowsAnswer } from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { repeatRecordsFromPages } from '@aglyn/tenant-runtime/repeat-record-pages'\nimport {\n collection,\n doc,\n type DocumentReference,\n type DocumentSnapshot,\n documentId,\n type Firestore,\n getDoc,\n getDocs,\n limit,\n orderBy,\n query,\n type QuerySnapshot,\n where,\n} from 'firebase/firestore'\n\nexport interface DatasetRepeatRowsRequest {\n firestore: Firestore\n /** `['orgs', orgId]` — where the site's organization keeps its datasets. */\n scope: readonly [string, string]\n /** The site the canvas edits. */\n hostId: string\n /** The dataset key exactly as the node stores it: an id or a display name. */\n key: string\n}\n\n/**\n * A dataset's rows for the besigner's canvas preview, read the way the\n * published page's composition reads them (`getDatasets` in the tenant\n * runtime), with the client SDK instead of the Admin one (AGL-3111).\n *\n * - The key resolves as the page resolves it: an id first, then a display\n * name within what this site may see. An id that exists but that this site\n * cannot see does not fall through to the name — that would answer \"which\n * dataset is called X\" for a key that already named a specific one.\n * - A dataset shared with other sites but not this one, or deleted, answers\n * `missing`: the page renders the element once, as written.\n * - The rows are the two bounded reads and the one ordering rule the page\n * uses, {@link repeatRecordsFromPages}, so a canvas copy is a row the page\n * renders.\n * - One reference hop (AGL-180) is loaded as the page loads it, so\n * `{{item.author.name}}` previews too.\n */\nexport async function readDatasetRepeatRows(\n request: DatasetRepeatRowsRequest,\n): Promise<RepeatRowsAnswer> {\n const { firestore, scope, hostId } = request\n const key = request.key.trim()\n if (!key) return { status: 'missing' }\n const datasets = collection(firestore, scope[0], scope[1], 'datasets')\n const usable = (snapshot: DocumentSnapshot | undefined) =>\n Boolean(snapshot?.exists()) &&\n !snapshot?.get('deletedAt') &&\n visibleToHost(snapshot?.get('visibleTo'), hostId)\n\n let dataset: DocumentSnapshot | undefined\n let resolvedById = false\n if (!key.includes('/')) {\n const byId = await getDoc(doc(datasets, key))\n if (byId.exists()) {\n resolvedById = true\n dataset = usable(byId) ? byId : undefined\n }\n }\n if (!resolvedById) {\n const byName = await getDocs(\n query(\n datasets,\n where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)),\n where('displayName', '==', key),\n limit(1),\n ),\n )\n dataset = byName.docs.find((snapshot) => usable(snapshot))\n }\n if (!dataset) return { status: 'missing' }\n\n const load = async (snapshot: DocumentSnapshot): Promise<RepeatableDataset> => ({\n records: await readRepeatRows(snapshot.ref),\n model: effectiveDatasetModel(snapshot.data() as HostDataset),\n })\n const rows = await load(dataset)\n const rowsByKey: Record<string, RepeatableDataset> = {\n [key]: rows,\n [dataset.id]: rows,\n }\n\n const targets = new Set<string>()\n for (const fieldId of rows.model?.order ?? []) {\n const field = rows.model?.fields[fieldId]\n const targetId =\n field?.type === 'reference' ? field.reference?.datasetId : undefined\n if (targetId && !targetId.includes('/') && !rowsByKey[targetId]) {\n targets.add(targetId)\n }\n }\n await Promise.all(\n [...targets].map(async (targetId) => {\n // Per target, like the page: a hop that cannot be read leaves its\n // tokens literal without costing the rows it hangs off.\n try {\n const target = await getDoc(doc(datasets, targetId))\n if (usable(target)) rowsByKey[targetId] = await load(target)\n } catch (error) {\n console.error(error)\n }\n }),\n )\n\n return {\n status: 'ready',\n label: datasetDisplayName(dataset.data() as HostDataset) || key,\n rowsByKey,\n }\n}\n\n/** Both bounded reads, the second only when the first came back short. */\nasync function readRepeatRows(\n datasetRef: DocumentReference,\n): Promise<Array<Record<string, unknown>>> {\n const records = collection(datasetRef, 'records')\n const page = (snapshot: QuerySnapshot) =>\n snapshot.docs.map((record) => ({\n id: record.id,\n data: record.data() as HostDatasetRecord,\n }))\n const byOrder = await getDocs(\n query(records, orderBy('order'), limit(REPEAT_MAX_RECORDS)),\n )\n const byId =\n byOrder.docs.length < REPEAT_MAX_RECORDS\n ? await getDocs(\n query(records, orderBy(documentId()), limit(REPEAT_MAX_RECORDS)),\n )\n : undefined\n return repeatRecordsFromPages(page(byOrder), byId ? page(byId) : [])\n}\n"],"names":["datasetDisplayName","effectiveDatasetModel","REPEAT_MAX_RECORDS","scopeTokensForHost","visibleToHost","repeatRecordsFromPages","collection","doc","documentId","getDoc","getDocs","limit","orderBy","query","where","readDatasetRepeatRows","request","rows","firestore","scope","hostId","key","trim","status","datasets","usable","snapshot","Boolean","exists","get","dataset","resolvedById","includes","byId","undefined","byName","docs","find","load","records","readRepeatRows","ref","model","data","rowsByKey","id","targets","Set","fieldId","order","field","fields","targetId","type","reference","datasetId","add","Promise","all","map","target","error","console","label","datasetRef","page","record","byOrder","length"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/repeat/dataset-repeat-rows.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n datasetDisplayName,\n effectiveDatasetModel,\n type HostDataset,\n type HostDatasetRecord,\n REPEAT_MAX_RECORDS,\n type RepeatableDataset,\n scopeTokensForHost,\n visibleToHost,\n} from '@aglyn/aglyn'\nimport type { RepeatRowsAnswer } from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { repeatRecordsFromPages } from '@aglyn/tenant-runtime/repeat-record-pages'\nimport {\n collection,\n doc,\n type DocumentReference,\n type DocumentSnapshot,\n documentId,\n type Firestore,\n getDoc,\n getDocs,\n limit,\n orderBy,\n query,\n type QuerySnapshot,\n where,\n} from 'firebase/firestore'\n\nexport interface DatasetRepeatRowsRequest {\n firestore: Firestore\n /** `['orgs', orgId]` — where the site's organization keeps its datasets. */\n scope: readonly [string, string]\n /** The site the canvas edits. */\n hostId: string\n /** The dataset key exactly as the node stores it: an id or a display name. */\n key: string\n}\n\n/**\n * A dataset's rows for the besigner's canvas preview, read the way the\n * published page's composition reads them (`readPublishedDatasetRows`, beside\n * this file), with the client SDK instead of the Admin one (AGL-3111).\n *\n * - The key resolves as the page resolves it: an id first, then a display\n * name within what this site may see. An id that exists but that this site\n * cannot see does not fall through to the name — that would answer \"which\n * dataset is called X\" for a key that already named a specific one.\n * - A dataset shared with other sites but not this one, or deleted, answers\n * `missing`: the page renders the element once, as written.\n * - The rows are the two bounded reads and the one ordering rule the page\n * uses, {@link repeatRecordsFromPages}, so a canvas copy is a row the page\n * renders.\n * - One reference hop (AGL-180) is loaded as the page loads it, so\n * `{{item.author.name}}` previews too.\n */\nexport async function readDatasetRepeatRows(\n request: DatasetRepeatRowsRequest,\n): Promise<RepeatRowsAnswer> {\n const { firestore, scope, hostId } = request\n const key = request.key.trim()\n if (!key) return { status: 'missing' }\n const datasets = collection(firestore, scope[0], scope[1], 'datasets')\n const usable = (snapshot: DocumentSnapshot | undefined) =>\n Boolean(snapshot?.exists()) &&\n !snapshot?.get('deletedAt') &&\n visibleToHost(snapshot?.get('visibleTo'), hostId)\n\n let dataset: DocumentSnapshot | undefined\n let resolvedById = false\n if (!key.includes('/')) {\n const byId = await getDoc(doc(datasets, key))\n if (byId.exists()) {\n resolvedById = true\n dataset = usable(byId) ? byId : undefined\n }\n }\n if (!resolvedById) {\n const byName = await getDocs(\n query(\n datasets,\n where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)),\n where('displayName', '==', key),\n limit(1),\n ),\n )\n dataset = byName.docs.find((snapshot) => usable(snapshot))\n }\n if (!dataset) return { status: 'missing' }\n\n const load = async (snapshot: DocumentSnapshot): Promise<RepeatableDataset> => ({\n records: await readRepeatRows(snapshot.ref),\n model: effectiveDatasetModel(snapshot.data() as HostDataset),\n })\n const rows = await load(dataset)\n const rowsByKey: Record<string, RepeatableDataset> = {\n [key]: rows,\n [dataset.id]: rows,\n }\n\n const targets = new Set<string>()\n for (const fieldId of rows.model?.order ?? []) {\n const field = rows.model?.fields[fieldId]\n const targetId =\n field?.type === 'reference' ? field.reference?.datasetId : undefined\n if (targetId && !targetId.includes('/') && !rowsByKey[targetId]) {\n targets.add(targetId)\n }\n }\n await Promise.all(\n [...targets].map(async (targetId) => {\n // Per target, like the page: a hop that cannot be read leaves its\n // tokens literal without costing the rows it hangs off.\n try {\n const target = await getDoc(doc(datasets, targetId))\n if (usable(target)) rowsByKey[targetId] = await load(target)\n } catch (error) {\n console.error(error)\n }\n }),\n )\n\n return {\n status: 'ready',\n label: datasetDisplayName(dataset.data() as HostDataset) || key,\n rowsByKey,\n }\n}\n\n/** Both bounded reads, the second only when the first came back short. */\nasync function readRepeatRows(\n datasetRef: DocumentReference,\n): Promise<Array<Record<string, unknown>>> {\n const records = collection(datasetRef, 'records')\n const page = (snapshot: QuerySnapshot) =>\n snapshot.docs.map((record) => ({\n id: record.id,\n data: record.data() as HostDatasetRecord,\n }))\n const byOrder = await getDocs(\n query(records, orderBy('order'), limit(REPEAT_MAX_RECORDS)),\n )\n const byId =\n byOrder.docs.length < REPEAT_MAX_RECORDS\n ? await getDocs(\n query(records, orderBy(documentId()), limit(REPEAT_MAX_RECORDS)),\n )\n : undefined\n return repeatRecordsFromPages(page(byOrder), byId ? page(byId) : [])\n}\n"],"names":["datasetDisplayName","effectiveDatasetModel","REPEAT_MAX_RECORDS","scopeTokensForHost","visibleToHost","repeatRecordsFromPages","collection","doc","documentId","getDoc","getDocs","limit","orderBy","query","where","readDatasetRepeatRows","request","rows","firestore","scope","hostId","key","trim","status","datasets","usable","snapshot","Boolean","exists","get","dataset","resolvedById","includes","byId","undefined","byName","docs","find","load","records","readRepeatRows","ref","model","data","rowsByKey","id","targets","Set","fieldId","order","field","fields","targetId","type","reference","datasetId","add","Promise","all","map","target","error","console","label","datasetRef","page","record","byOrder","length"],"mappings":"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"}
@@ -0,0 +1,43 @@
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 { RepeatRowsAnswer, RepeatRowsRequest } from '@aglyn/aglyn/plugin-manager/repeat-rows';
18
+ /**
19
+ * The datasets a published page repeats over, with their records — this
20
+ * plugin's answer to the platform's repeat-rows contract
21
+ * (`plugin-manager/repeat-rows.ts`), registered from
22
+ * `registerDataServerDeclarations` and read by the tenant composition.
23
+ *
24
+ * `keys` are the page's repeat keys (`repeatKeys`), each a dataset id or
25
+ * a display name — editors type the friendly name into the Repeat attribute.
26
+ * The map answers under every key exactly as written, and under every loaded
27
+ * dataset's id, which is how a reference hop (AGL-180) finds its target.
28
+ * Records are editor-ordered and capped at the repeat bound.
29
+ *
30
+ * Only the datasets the page names are read. A site may see thousands of
31
+ * datasets, and a read of "the site's datasets" can only ever be a bounded
32
+ * page of them, so a repeat bound to one outside that page rendered its
33
+ * template once with nothing to say why.
34
+ *
35
+ * Fail-open, per key: a key that cannot be read is left out, and its
36
+ * repeatable renders its template untouched, without costing the others.
37
+ *
38
+ * Scoped to what THIS host may see (AGL-1039): a display name resolves inside
39
+ * the host's scope, and an id outside it resolves to nothing. The Admin SDK
40
+ * does not evaluate rules, so AGL-1041 does not cover this path; the filter
41
+ * has to live here.
42
+ */
43
+ export declare function readPublishedDatasetRows(options: RepeatRowsRequest): Promise<RepeatRowsAnswer>;
@@ -0,0 +1,170 @@
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 { effectiveDatasetModel, REPEAT_MAX_RECORDS, visibleToHost } from "@aglyn/aglyn/server";
17
+ import { orgDataQueryForHost } from "@aglyn/tenant-data-admin";
18
+ import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
19
+ import { repeatRecordsFromPages } from "@aglyn/tenant-runtime/repeat-record-pages";
20
+ import { FieldPath } from "firebase-admin/firestore";
21
+ /**
22
+ * The render path's largest read (AGL-1302): up to two pages of records per
23
+ * dataset a page repeats over. Records are mapped to plain values before
24
+ * return, so the cached shape is what repeatable expansion consumes. Backstop
25
+ * only; the publish path busts the tag — see PUBLISHED_SITE_DATA_TTL_SECONDS.
26
+ */ const DATASETS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS;
27
+ /**
28
+ * The datasets a published page repeats over, with their records — this
29
+ * plugin's answer to the platform's repeat-rows contract
30
+ * (`plugin-manager/repeat-rows.ts`), registered from
31
+ * `registerDataServerDeclarations` and read by the tenant composition.
32
+ *
33
+ * `keys` are the page's repeat keys (`repeatKeys`), each a dataset id or
34
+ * a display name — editors type the friendly name into the Repeat attribute.
35
+ * The map answers under every key exactly as written, and under every loaded
36
+ * dataset's id, which is how a reference hop (AGL-180) finds its target.
37
+ * Records are editor-ordered and capped at the repeat bound.
38
+ *
39
+ * Only the datasets the page names are read. A site may see thousands of
40
+ * datasets, and a read of "the site's datasets" can only ever be a bounded
41
+ * page of them, so a repeat bound to one outside that page rendered its
42
+ * template once with nothing to say why.
43
+ *
44
+ * Fail-open, per key: a key that cannot be read is left out, and its
45
+ * repeatable renders its template untouched, without costing the others.
46
+ *
47
+ * Scoped to what THIS host may see (AGL-1039): a display name resolves inside
48
+ * the host's scope, and an id outside it resolves to nothing. The Admin SDK
49
+ * does not evaluate rules, so AGL-1041 does not cover this path; the filter
50
+ * has to live here.
51
+ */ export async function readPublishedDatasetRows(options) {
52
+ const keys = [
53
+ ...new Set(options.keys.map((key)=>key.trim()).filter(Boolean))
54
+ ].sort();
55
+ if (!keys.length) return {};
56
+ try {
57
+ return await withRenderCache({
58
+ key: [
59
+ 'tenant-datasets',
60
+ options.hostId,
61
+ ...keys
62
+ ],
63
+ revalidate: DATASETS_TTL_SECONDS,
64
+ tags: [
65
+ tenantDataTag(options.hostId)
66
+ ],
67
+ read: ()=>readDatasets(options.hostId, keys)
68
+ });
69
+ } catch (error) {
70
+ console.error(error);
71
+ return readDatasets(options.hostId, keys);
72
+ }
73
+ }
74
+ async function readDatasets(hostId, keys) {
75
+ const datasets = {};
76
+ try {
77
+ var _ref_parent_parent, _ref_parent;
78
+ const { ref, query } = await orgDataQueryForHost(hostId, 'datasets');
79
+ // The same visibility rule `resolveDatasetDoc` applies: a keyed read
80
+ // bypasses the scoped query, so the scope is checked on the document.
81
+ const orgScoped = ((_ref_parent = ref.parent) == null ? void 0 : (_ref_parent_parent = _ref_parent.parent) == null ? void 0 : _ref_parent_parent.id) === 'orgs';
82
+ const usable = (snapshot)=>Boolean(snapshot == null ? void 0 : snapshot.exists) && !(snapshot == null ? void 0 : snapshot.get('deletedAt')) && (!orgScoped || visibleToHost(snapshot == null ? void 0 : snapshot.get('visibleTo'), hostId));
83
+ const loads = new Map();
84
+ const load = (snapshot)=>{
85
+ let dataset = loads.get(snapshot.id);
86
+ if (!dataset) {
87
+ dataset = readRepeatRecords(snapshot.ref).then((records)=>({
88
+ records,
89
+ model: effectiveDatasetModel(snapshot.data())
90
+ }));
91
+ loads.set(snapshot.id, dataset);
92
+ }
93
+ return dataset;
94
+ };
95
+ // An id first, then a display name. An id that exists but that this host
96
+ // cannot see does not fall through to the name: that would answer "which
97
+ // dataset is called X" for a key that already named a specific one.
98
+ const resolveKey = async (key)=>{
99
+ if (!key.includes('/')) {
100
+ const byId = await ref.doc(key).get();
101
+ if (byId.exists) return usable(byId) ? byId : undefined;
102
+ }
103
+ const byName = await query.where('displayName', '==', key).limit(1).get();
104
+ return byName.docs.find((snapshot)=>usable(snapshot));
105
+ };
106
+ const resolved = await Promise.all(keys.map(async (key)=>{
107
+ try {
108
+ const snapshot = await resolveKey(key);
109
+ if (!snapshot) return undefined;
110
+ const dataset = await load(snapshot);
111
+ datasets[key] = dataset;
112
+ datasets[snapshot.id] = dataset;
113
+ return dataset;
114
+ } catch (error) {
115
+ console.error(error);
116
+ return undefined;
117
+ }
118
+ }));
119
+ // One reference hop (AGL-180): `{{item.author.name}}` reads the target's
120
+ // rows by id, so the target has to be loaded even though no repeat names it.
121
+ const targets = new Set();
122
+ for (const dataset of resolved){
123
+ var _ref;
124
+ var _dataset_model;
125
+ for (const fieldId of (_ref = dataset == null ? void 0 : (_dataset_model = dataset.model) == null ? void 0 : _dataset_model.order) != null ? _ref : []){
126
+ var _dataset_model1, _field_reference;
127
+ const field = dataset == null ? void 0 : (_dataset_model1 = dataset.model) == null ? void 0 : _dataset_model1.fields[fieldId];
128
+ const targetId = (field == null ? void 0 : field.type) === 'reference' ? (_field_reference = field.reference) == null ? void 0 : _field_reference.datasetId : undefined;
129
+ if (targetId && !targetId.includes('/') && !datasets[targetId]) {
130
+ targets.add(targetId);
131
+ }
132
+ }
133
+ }
134
+ await Promise.all([
135
+ ...targets
136
+ ].map(async (targetId)=>{
137
+ try {
138
+ const target = await ref.doc(targetId).get();
139
+ if (usable(target)) datasets[targetId] = await load(target);
140
+ } catch (error) {
141
+ console.error(error);
142
+ }
143
+ }));
144
+ } catch (error) {
145
+ console.error(error);
146
+ }
147
+ return datasets;
148
+ }
149
+ /**
150
+ * The records a repeat renders, in the order it renders them.
151
+ *
152
+ * `orderBy('order')` reads the first group, and matches only documents that
153
+ * carry the field. When it comes back short, every ordered record is already
154
+ * in hand — so at most that many rows of the first page by document id can be
155
+ * ordered, and the rest of that page is exactly the first unordered records.
156
+ * Two bounded reads, never a walk of the collection; which rows win, and in
157
+ * what order, is {@link repeatRecordsFromPages}, the rule the besigner's
158
+ * canvas preview reads through as well (AGL-3111).
159
+ */ async function readRepeatRecords(datasetRef) {
160
+ const recordsRef = datasetRef.collection('records');
161
+ const page = (snapshot)=>snapshot.docs.map((doc)=>({
162
+ id: doc.id,
163
+ data: doc.data()
164
+ }));
165
+ const byOrder = await recordsRef.orderBy('order').limit(REPEAT_MAX_RECORDS).get();
166
+ const byId = byOrder.docs.length < REPEAT_MAX_RECORDS ? await recordsRef.orderBy(FieldPath.documentId()).limit(REPEAT_MAX_RECORDS).get() : undefined;
167
+ return repeatRecordsFromPages(page(byOrder), byId ? page(byId) : []);
168
+ }
169
+
170
+ //# sourceMappingURL=dataset-repeat-rows.server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/repeat/dataset-repeat-rows.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n effectiveDatasetModel,\n type HostDataset,\n type HostDatasetRecord,\n REPEAT_MAX_RECORDS,\n type RepeatableDataset,\n visibleToHost,\n} from '@aglyn/aglyn/server'\nimport type {\n RepeatRowsAnswer,\n RepeatRowsRequest,\n} from '@aglyn/aglyn/plugin-manager/repeat-rows'\nimport { orgDataQueryForHost } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\nimport { repeatRecordsFromPages } from '@aglyn/tenant-runtime/repeat-record-pages'\nimport { FieldPath } from 'firebase-admin/firestore'\n\n/**\n * The render path's largest read (AGL-1302): up to two pages of records per\n * dataset a page repeats over. Records are mapped to plain values before\n * return, so the cached shape is what repeatable expansion consumes. Backstop\n * only; the publish path busts the tag — see PUBLISHED_SITE_DATA_TTL_SECONDS.\n */\nconst DATASETS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/**\n * The datasets a published page repeats over, with their records — this\n * plugin's answer to the platform's repeat-rows contract\n * (`plugin-manager/repeat-rows.ts`), registered from\n * `registerDataServerDeclarations` and read by the tenant composition.\n *\n * `keys` are the page's repeat keys (`repeatKeys`), each a dataset id or\n * a display name — editors type the friendly name into the Repeat attribute.\n * The map answers under every key exactly as written, and under every loaded\n * dataset's id, which is how a reference hop (AGL-180) finds its target.\n * Records are editor-ordered and capped at the repeat bound.\n *\n * Only the datasets the page names are read. A site may see thousands of\n * datasets, and a read of \"the site's datasets\" can only ever be a bounded\n * page of them, so a repeat bound to one outside that page rendered its\n * template once with nothing to say why.\n *\n * Fail-open, per key: a key that cannot be read is left out, and its\n * repeatable renders its template untouched, without costing the others.\n *\n * Scoped to what THIS host may see (AGL-1039): a display name resolves inside\n * the host's scope, and an id outside it resolves to nothing. The Admin SDK\n * does not evaluate rules, so AGL-1041 does not cover this path; the filter\n * has to live here.\n */\nexport async function readPublishedDatasetRows(\n options: RepeatRowsRequest,\n): Promise<RepeatRowsAnswer> {\n const keys = [\n ...new Set(options.keys.map((key) => key.trim()).filter(Boolean)),\n ].sort()\n if (!keys.length) return {}\n try {\n return await withRenderCache({\n key: ['tenant-datasets', options.hostId, ...keys],\n revalidate: DATASETS_TTL_SECONDS,\n tags: [tenantDataTag(options.hostId)],\n read: () => readDatasets(options.hostId, keys),\n })\n } catch (error) {\n console.error(error)\n return readDatasets(options.hostId, keys)\n }\n}\n\nasync function readDatasets(\n hostId: string,\n keys: readonly string[],\n): Promise<Record<string, RepeatableDataset>> {\n const datasets: Record<string, RepeatableDataset> = {}\n try {\n const { ref, query } = await orgDataQueryForHost(hostId, 'datasets')\n // The same visibility rule `resolveDatasetDoc` applies: a keyed read\n // bypasses the scoped query, so the scope is checked on the document.\n const orgScoped = ref.parent?.parent?.id === 'orgs'\n const usable = (snapshot: FirebaseFirestore.DocumentSnapshot | undefined) =>\n Boolean(snapshot?.exists) &&\n !snapshot?.get('deletedAt') &&\n (!orgScoped || visibleToHost(snapshot?.get('visibleTo'), hostId))\n\n const loads = new Map<string, Promise<RepeatableDataset>>()\n const load = (snapshot: FirebaseFirestore.DocumentSnapshot) => {\n let dataset = loads.get(snapshot.id)\n if (!dataset) {\n dataset = readRepeatRecords(snapshot.ref).then((records) => ({\n records,\n model: effectiveDatasetModel(\n snapshot.data() as HostDataset,\n ),\n }))\n loads.set(snapshot.id, dataset)\n }\n return dataset\n }\n\n // An id first, then a display name. An id that exists but that this host\n // cannot see does not fall through to the name: that would answer \"which\n // dataset is called X\" for a key that already named a specific one.\n const resolveKey = async (key: string) => {\n if (!key.includes('/')) {\n const byId = await ref.doc(key).get()\n if (byId.exists) return usable(byId) ? byId : undefined\n }\n const byName = await query.where('displayName', '==', key).limit(1).get()\n return byName.docs.find((snapshot) => usable(snapshot))\n }\n\n const resolved = await Promise.all(\n keys.map(async (key) => {\n try {\n const snapshot = await resolveKey(key)\n if (!snapshot) return undefined\n const dataset = await load(snapshot)\n datasets[key] = dataset\n datasets[snapshot.id] = dataset\n return dataset\n } catch (error) {\n console.error(error)\n return undefined\n }\n }),\n )\n\n // One reference hop (AGL-180): `{{item.author.name}}` reads the target's\n // rows by id, so the target has to be loaded even though no repeat names it.\n const targets = new Set<string>()\n for (const dataset of resolved) {\n for (const fieldId of dataset?.model?.order ?? []) {\n const field = dataset?.model?.fields[fieldId]\n const targetId =\n field?.type === 'reference' ? field.reference?.datasetId : undefined\n if (targetId && !targetId.includes('/') && !datasets[targetId]) {\n targets.add(targetId)\n }\n }\n }\n await Promise.all(\n [...targets].map(async (targetId) => {\n try {\n const target = await ref.doc(targetId).get()\n if (usable(target)) datasets[targetId] = await load(target)\n } catch (error) {\n console.error(error)\n }\n }),\n )\n } catch (error) {\n console.error(error)\n }\n return datasets\n}\n\n/**\n * The records a repeat renders, in the order it renders them.\n *\n * `orderBy('order')` reads the first group, and matches only documents that\n * carry the field. When it comes back short, every ordered record is already\n * in hand — so at most that many rows of the first page by document id can be\n * ordered, and the rest of that page is exactly the first unordered records.\n * Two bounded reads, never a walk of the collection; which rows win, and in\n * what order, is {@link repeatRecordsFromPages}, the rule the besigner's\n * canvas preview reads through as well (AGL-3111).\n */\nasync function readRepeatRecords(\n datasetRef: FirebaseFirestore.DocumentReference,\n): Promise<Array<Record<string, unknown>>> {\n const recordsRef = datasetRef.collection('records')\n const page = (snapshot: FirebaseFirestore.QuerySnapshot) =>\n snapshot.docs.map((doc) => ({\n id: doc.id,\n data: doc.data() as HostDatasetRecord,\n }))\n const byOrder = await recordsRef\n .orderBy('order')\n .limit(REPEAT_MAX_RECORDS)\n .get()\n const byId =\n byOrder.docs.length < REPEAT_MAX_RECORDS\n ? await recordsRef\n .orderBy(FieldPath.documentId())\n .limit(REPEAT_MAX_RECORDS)\n .get()\n : undefined\n return repeatRecordsFromPages(page(byOrder), byId ? page(byId) : [])\n}\n"],"names":["effectiveDatasetModel","REPEAT_MAX_RECORDS","visibleToHost","orgDataQueryForHost","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","repeatRecordsFromPages","FieldPath","DATASETS_TTL_SECONDS","readPublishedDatasetRows","options","keys","Set","map","key","trim","filter","Boolean","sort","length","hostId","revalidate","tags","read","readDatasets","error","console","datasets","ref","query","orgScoped","parent","id","usable","snapshot","exists","get","loads","Map","load","dataset","readRepeatRecords","then","records","model","data","set","resolveKey","includes","byId","doc","undefined","byName","where","limit","docs","find","resolved","Promise","all","targets","fieldId","order","field","fields","targetId","type","reference","datasetId","add","target","datasetRef","recordsRef","collection","page","byOrder","orderBy","documentId"],"mappings":"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"}
@@ -16,6 +16,7 @@
16
16
  */ import { FieldComponentType } from "@aglyn/aglyn";
17
17
  import { useFirestore, useOrgDataScope } from "@aglyn/tenant-feature-instance";
18
18
  import { useEffect, useState } from "react";
19
+ import { DATASET_REPEAT_SOURCE_ID } from "../constants/bundle-common.js";
19
20
  import { readDatasetRepeatRows } from "./dataset-repeat-rows.js";
20
21
  /**
21
22
  * The prop a repeat's dataset persists under (AGL-103). The published page's
@@ -81,9 +82,10 @@ import { readDatasetRepeatRows } from "./dataset-repeat-rows.js";
81
82
  * rows. Registered by {@link registerDataConsole}, the function the console's
82
83
  * plugin loader calls by name.
83
84
  */ export const DATASET_REPEAT_SOURCE = {
84
- id: 'dataset',
85
+ id: DATASET_REPEAT_SOURCE_ID,
85
86
  label: 'Dataset',
86
87
  keyProp: DATASET_REPEAT_KEY_PROP,
88
+ entityKind: 'datasets',
87
89
  keyAttribute: {
88
90
  component: FieldComponentType.DATASET_SELECT,
89
91
  label: 'Repeat over dataset',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/repeat/dataset-repeat-source.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 { FieldComponentType } from '@aglyn/aglyn'\nimport type {\n RepeatRowsAnswer,\n RepeatRowsRequest,\n RepeatSource,\n} from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { useFirestore, useOrgDataScope } from '@aglyn/tenant-feature-instance'\nimport { useEffect, useState } from 'react'\nimport { readDatasetRepeatRows } from './dataset-repeat-rows'\n\n/**\n * The prop a repeat's dataset persists under (AGL-103). The published page's\n * composition expands every node that carries it, whatever its component, so\n * the Stack's stored repeats and any element's read as one shape. Persisted —\n * never rename.\n */\nexport const DATASET_REPEAT_KEY_PROP = 'repeatDataset'\n\n/**\n * A dataset's rows for the canvas, read once per key and site while a canvas\n * element repeats over it.\n *\n * `missing` for a site with no organization: datasets belong to the\n * organization, so there is nothing such a site can repeat over.\n */\nexport function useDatasetRepeatRows(\n request: RepeatRowsRequest,\n): RepeatRowsAnswer {\n const { hostId, key } = request\n const firestore = useFirestore()\n const { scope, ready } = useOrgDataScope({ hostId })\n const [answer, setAnswer] = useState<RepeatRowsAnswer>({ status: 'loading' })\n useEffect(() => {\n if (!ready) return undefined\n if (!scope) {\n setAnswer({ status: 'missing' })\n return undefined\n }\n let active = true\n setAnswer({ status: 'loading' })\n readDatasetRepeatRows({ firestore, scope, hostId, key }).then(\n (next) => {\n if (active) setAnswer(next)\n },\n (error) => {\n console.error(error)\n if (active) setAnswer({ status: 'error' })\n },\n )\n return () => {\n active = false\n }\n }, [firestore, scope, ready, hostId, key])\n return answer\n}\n\n/**\n * Datasets as a repeat source (AGL-3111): what any element can repeat over,\n * picked with the dataset picker and previewed on the canvas from the real\n * rows. Registered by {@link registerDataConsole}, the function the console's\n * plugin loader calls by name.\n */\nexport const DATASET_REPEAT_SOURCE: RepeatSource = {\n id: 'dataset',\n label: 'Dataset',\n keyProp: DATASET_REPEAT_KEY_PROP,\n keyAttribute: {\n component: FieldComponentType.DATASET_SELECT,\n label: 'Repeat over dataset',\n description:\n 'Render this element once per record of a dataset on the published ' +\n 'site. Use {{item.field}} for a record value. Stored by dataset id — ' +\n 'renames never break the repeat.',\n },\n useRows: useDatasetRepeatRows,\n}\n"],"names":["FieldComponentType","useFirestore","useOrgDataScope","useEffect","useState","readDatasetRepeatRows","DATASET_REPEAT_KEY_PROP","useDatasetRepeatRows","request","hostId","key","firestore","scope","ready","answer","setAnswer","status","undefined","active","then","next","error","console","DATASET_REPEAT_SOURCE","id","label","keyProp","keyAttribute","component","DATASET_SELECT","description","useRows"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,eAAc;AAMjD,SAASC,YAAY,EAAEC,eAAe,QAAQ,iCAAgC;AAC9E,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,qBAAqB,QAAQ,2BAAuB;AAE7D;;;;;CAKC,GACD,OAAO,MAAMC,0BAA0B,gBAAe;AAEtD;;;;;;CAMC,GACD,OAAO,SAASC,qBACdC,OAA0B;IAE1B,MAAM,EAAEC,MAAM,EAAEC,GAAG,EAAE,GAAGF;IACxB,MAAMG,YAAYV;IAClB,MAAM,EAAEW,KAAK,EAAEC,KAAK,EAAE,GAAGX,gBAAgB;QAAEO;IAAO;IAClD,MAAM,CAACK,QAAQC,UAAU,GAAGX,SAA2B;QAAEY,QAAQ;IAAU;IAC3Eb,UAAU;QACR,IAAI,CAACU,OAAO,OAAOI;QACnB,IAAI,CAACL,OAAO;YACVG,UAAU;gBAAEC,QAAQ;YAAU;YAC9B,OAAOC;QACT;QACA,IAAIC,SAAS;QACbH,UAAU;YAAEC,QAAQ;QAAU;QAC9BX,sBAAsB;YAAEM;YAAWC;YAAOH;YAAQC;QAAI,GAAGS,IAAI,CAC3D,CAACC;YACC,IAAIF,QAAQH,UAAUK;QACxB,GACA,CAACC;YACCC,QAAQD,KAAK,CAACA;YACd,IAAIH,QAAQH,UAAU;gBAAEC,QAAQ;YAAQ;QAC1C;QAEF,OAAO;YACLE,SAAS;QACX;IACF,GAAG;QAACP;QAAWC;QAAOC;QAAOJ;QAAQC;KAAI;IACzC,OAAOI;AACT;AAEA;;;;;CAKC,GACD,OAAO,MAAMS,wBAAsC;IACjDC,IAAI;IACJC,OAAO;IACPC,SAASpB;IACTqB,cAAc;QACZC,WAAW5B,mBAAmB6B,cAAc;QAC5CJ,OAAO;QACPK,aACE,uEACA,yEACA;IACJ;IACAC,SAASxB;AACX,EAAC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/repeat/dataset-repeat-source.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 { FieldComponentType } from '@aglyn/aglyn'\nimport type {\n RepeatRowsAnswer,\n RepeatRowsRequest,\n RepeatSource,\n} from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { useFirestore, useOrgDataScope } from '@aglyn/tenant-feature-instance'\nimport { useEffect, useState } from 'react'\nimport { DATASET_REPEAT_SOURCE_ID } from '../constants/bundle-common'\nimport { readDatasetRepeatRows } from './dataset-repeat-rows'\n\n/**\n * The prop a repeat's dataset persists under (AGL-103). The published page's\n * composition expands every node that carries it, whatever its component, so\n * the Stack's stored repeats and any element's read as one shape. Persisted —\n * never rename.\n */\nexport const DATASET_REPEAT_KEY_PROP = 'repeatDataset'\n\n/**\n * A dataset's rows for the canvas, read once per key and site while a canvas\n * element repeats over it.\n *\n * `missing` for a site with no organization: datasets belong to the\n * organization, so there is nothing such a site can repeat over.\n */\nexport function useDatasetRepeatRows(\n request: RepeatRowsRequest,\n): RepeatRowsAnswer {\n const { hostId, key } = request\n const firestore = useFirestore()\n const { scope, ready } = useOrgDataScope({ hostId })\n const [answer, setAnswer] = useState<RepeatRowsAnswer>({ status: 'loading' })\n useEffect(() => {\n if (!ready) return undefined\n if (!scope) {\n setAnswer({ status: 'missing' })\n return undefined\n }\n let active = true\n setAnswer({ status: 'loading' })\n readDatasetRepeatRows({ firestore, scope, hostId, key }).then(\n (next) => {\n if (active) setAnswer(next)\n },\n (error) => {\n console.error(error)\n if (active) setAnswer({ status: 'error' })\n },\n )\n return () => {\n active = false\n }\n }, [firestore, scope, ready, hostId, key])\n return answer\n}\n\n/**\n * Datasets as a repeat source (AGL-3111): what any element can repeat over,\n * picked with the dataset picker and previewed on the canvas from the real\n * rows. Registered by {@link registerDataConsole}, the function the console's\n * plugin loader calls by name.\n */\nexport const DATASET_REPEAT_SOURCE: RepeatSource = {\n id: DATASET_REPEAT_SOURCE_ID,\n label: 'Dataset',\n keyProp: DATASET_REPEAT_KEY_PROP,\n entityKind: 'datasets',\n keyAttribute: {\n component: FieldComponentType.DATASET_SELECT,\n label: 'Repeat over dataset',\n description:\n 'Render this element once per record of a dataset on the published ' +\n 'site. Use {{item.field}} for a record value. Stored by dataset id — ' +\n 'renames never break the repeat.',\n },\n useRows: useDatasetRepeatRows,\n}\n"],"names":["FieldComponentType","useFirestore","useOrgDataScope","useEffect","useState","DATASET_REPEAT_SOURCE_ID","readDatasetRepeatRows","DATASET_REPEAT_KEY_PROP","useDatasetRepeatRows","request","hostId","key","firestore","scope","ready","answer","setAnswer","status","undefined","active","then","next","error","console","DATASET_REPEAT_SOURCE","id","label","keyProp","entityKind","keyAttribute","component","DATASET_SELECT","description","useRows"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,eAAc;AAMjD,SAASC,YAAY,EAAEC,eAAe,QAAQ,iCAAgC;AAC9E,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,wBAAwB,QAAQ,gCAA4B;AACrE,SAASC,qBAAqB,QAAQ,2BAAuB;AAE7D;;;;;CAKC,GACD,OAAO,MAAMC,0BAA0B,gBAAe;AAEtD;;;;;;CAMC,GACD,OAAO,SAASC,qBACdC,OAA0B;IAE1B,MAAM,EAAEC,MAAM,EAAEC,GAAG,EAAE,GAAGF;IACxB,MAAMG,YAAYX;IAClB,MAAM,EAAEY,KAAK,EAAEC,KAAK,EAAE,GAAGZ,gBAAgB;QAAEQ;IAAO;IAClD,MAAM,CAACK,QAAQC,UAAU,GAAGZ,SAA2B;QAAEa,QAAQ;IAAU;IAC3Ed,UAAU;QACR,IAAI,CAACW,OAAO,OAAOI;QACnB,IAAI,CAACL,OAAO;YACVG,UAAU;gBAAEC,QAAQ;YAAU;YAC9B,OAAOC;QACT;QACA,IAAIC,SAAS;QACbH,UAAU;YAAEC,QAAQ;QAAU;QAC9BX,sBAAsB;YAAEM;YAAWC;YAAOH;YAAQC;QAAI,GAAGS,IAAI,CAC3D,CAACC;YACC,IAAIF,QAAQH,UAAUK;QACxB,GACA,CAACC;YACCC,QAAQD,KAAK,CAACA;YACd,IAAIH,QAAQH,UAAU;gBAAEC,QAAQ;YAAQ;QAC1C;QAEF,OAAO;YACLE,SAAS;QACX;IACF,GAAG;QAACP;QAAWC;QAAOC;QAAOJ;QAAQC;KAAI;IACzC,OAAOI;AACT;AAEA;;;;;CAKC,GACD,OAAO,MAAMS,wBAAsC;IACjDC,IAAIpB;IACJqB,OAAO;IACPC,SAASpB;IACTqB,YAAY;IACZC,cAAc;QACZC,WAAW9B,mBAAmB+B,cAAc;QAC5CL,OAAO;QACPM,aACE,uEACA,yEACA;IACJ;IACAC,SAASzB;AACX,EAAC"}