@aglyn/plugins-data 1.0.0-beta.218 → 1.0.0-beta.220

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 (84) hide show
  1. package/README.md +1 -1
  2. package/package.json +11 -9
  3. package/src/lib/constants/bundle-common.d.ts +8 -0
  4. package/src/lib/constants/bundle-common.js +6 -0
  5. package/src/lib/constants/bundle-common.js.map +1 -1
  6. package/src/lib/declarations.console-server.d.ts +47 -0
  7. package/src/lib/declarations.console-server.js +74 -0
  8. package/src/lib/declarations.console-server.js.map +1 -0
  9. package/src/lib/declarations.server.d.ts +43 -0
  10. package/src/lib/declarations.server.js +73 -0
  11. package/src/lib/declarations.server.js.map +1 -0
  12. package/src/lib/form-target/dataset-form-record-target.server.d.ts +40 -0
  13. package/src/lib/form-target/dataset-form-record-target.server.js +160 -0
  14. package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -0
  15. package/src/lib/form-target/form-dataset-binding-token.d.ts +35 -0
  16. package/src/lib/form-target/form-dataset-binding-token.js +107 -0
  17. package/src/lib/form-target/form-dataset-binding-token.js.map +1 -0
  18. package/src/lib/form-target/form-dataset-binding.d.ts +50 -0
  19. package/src/lib/form-target/form-dataset-binding.js +74 -0
  20. package/src/lib/form-target/form-dataset-binding.js.map +1 -0
  21. package/src/lib/form-target/stamp-form-dataset-bindings.d.ts +39 -0
  22. package/src/lib/form-target/stamp-form-dataset-bindings.js +65 -0
  23. package/src/lib/form-target/stamp-form-dataset-bindings.js.map +1 -0
  24. package/src/lib/model/dataset-csv.d.ts +36 -0
  25. package/src/lib/model/dataset-csv.js +85 -0
  26. package/src/lib/model/dataset-csv.js.map +1 -0
  27. package/src/lib/model/dataset-io.d.ts +8 -7
  28. package/src/lib/model/dataset-io.js +9 -8
  29. package/src/lib/model/dataset-io.js.map +1 -1
  30. package/src/lib/model/dataset-record-list.d.ts +40 -0
  31. package/src/lib/model/dataset-record-list.js +61 -0
  32. package/src/lib/model/dataset-record-list.js.map +1 -0
  33. package/src/lib/model/dataset-record.d.ts +33 -0
  34. package/src/lib/model/dataset-record.js +68 -0
  35. package/src/lib/model/dataset-record.js.map +1 -0
  36. package/src/lib/plugin.js +3 -0
  37. package/src/lib/plugin.js.map +1 -1
  38. package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -2
  39. package/src/lib/repeat/dataset-repeat-rows.js +2 -2
  40. package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
  41. package/src/lib/repeat/dataset-repeat-rows.server.d.ts +43 -0
  42. package/src/lib/repeat/dataset-repeat-rows.server.js +170 -0
  43. package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -0
  44. package/src/lib/repeat/dataset-repeat-source.js +3 -1
  45. package/src/lib/repeat/dataset-repeat-source.js.map +1 -1
  46. package/src/lib/server/announce-dataset-records.d.ts +37 -0
  47. package/src/lib/server/announce-dataset-records.js +54 -0
  48. package/src/lib/server/announce-dataset-records.js.map +1 -0
  49. package/src/lib/server/api-v1/datasets.d.ts +18 -0
  50. package/src/lib/server/api-v1/datasets.js +646 -0
  51. package/src/lib/server/api-v1/datasets.js.map +1 -0
  52. package/src/lib/server/api-v1/openapi.d.ts +26 -0
  53. package/src/lib/server/api-v1/openapi.js +260 -0
  54. package/src/lib/server/api-v1/openapi.js.map +1 -0
  55. package/src/lib/server/api-v1/usage.d.ts +26 -0
  56. package/src/lib/server/api-v1/usage.js +33 -0
  57. package/src/lib/server/api-v1/usage.js.map +1 -0
  58. package/src/lib/server/custom-field-types.d.ts +39 -0
  59. package/src/lib/server/custom-field-types.js +41 -0
  60. package/src/lib/server/custom-field-types.js.map +1 -0
  61. package/src/lib/server/dataset-figures.d.ts +61 -0
  62. package/src/lib/server/dataset-figures.js +413 -0
  63. package/src/lib/server/dataset-figures.js.map +1 -0
  64. package/src/lib/server/dataset-record-index.d.ts +18 -0
  65. package/src/lib/server/dataset-record-index.js +62 -0
  66. package/src/lib/server/dataset-record-index.js.map +1 -0
  67. package/src/lib/server/dataset-storage-meter.d.ts +29 -0
  68. package/src/lib/server/dataset-storage-meter.js +82 -0
  69. package/src/lib/server/dataset-storage-meter.js.map +1 -0
  70. package/src/lib/server/datasets-export-route.d.ts +57 -0
  71. package/src/lib/server/datasets-export-route.js +229 -0
  72. package/src/lib/server/datasets-export-route.js.map +1 -0
  73. package/src/lib/server/datasets-route.d.ts +38 -0
  74. package/src/lib/server/datasets-route.js +614 -0
  75. package/src/lib/server/datasets-route.js.map +1 -0
  76. package/src/lib/server.d.ts +23 -0
  77. package/src/lib/server.js +54 -0
  78. package/src/lib/server.js.map +1 -0
  79. package/src/lib/site-bundle/datasets-site-bundle.server.d.ts +87 -0
  80. package/src/lib/site-bundle/datasets-site-bundle.server.js +184 -0
  81. package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -0
  82. package/src/lib/usage-axes.d.ts +24 -0
  83. package/src/lib/usage-axes.js +32 -0
  84. package/src/lib/usage-axes.js.map +1 -0
@@ -0,0 +1,40 @@
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 PluginRecordListSource } from '@aglyn/aglyn/plugin-manager/plugin-record-lists';
18
+ /**
19
+ * The workspace's datasets, listed in the console for another plugin's picker
20
+ * or check (AGL-3080) — an automation step choosing the dataset it writes to,
21
+ * a reference check confirming one still exists.
22
+ *
23
+ * Narrowed to a site (`hostId`), the query asks only for the datasets shared
24
+ * with it, by the site's scope tokens: an automation runs on the site, so it
25
+ * may only reach datasets the site can use, and the filter is the one the
26
+ * security rules require of a member scoped to the site. With no site, a
27
+ * member who is not organization-wide (`memberScope`) is asked for the ones
28
+ * shared with any site they reach — the rules refuse them an unfiltered
29
+ * list — and an organization-wide reader lists the organization's own.
30
+ * `installedFrom` keeps the ones installed from that listing, by the
31
+ * `source.listingId` every dataset install stamps.
32
+ *
33
+ * Ordered by document id, which the automatic index serves beside the
34
+ * `visibleTo` filter, and the composite index on (`source.listingId`,
35
+ * `visibleTo`) beside both. Each record is `datasetIndexedRecord`'s, the
36
+ * server index's shape.
37
+ */
38
+ export declare const datasetRecordListSource: PluginRecordListSource;
39
+ /** Called from the console registrar, owner named for a spec that calls it directly. */
40
+ export declare function registerDatasetRecordList(): void;
@@ -0,0 +1,61 @@
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 { scopeTokensForHost } from "@aglyn/aglyn/app-utils/scope-tokens";
17
+ import { registerPluginRecordListSource } from "@aglyn/aglyn/plugin-manager/plugin-record-lists";
18
+ import { collection, documentId, limit, orderBy, query, where } from "firebase/firestore";
19
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
20
+ import { datasetIndexedRecord } from "./dataset-record.js";
21
+ /**
22
+ * The workspace's datasets, listed in the console for another plugin's picker
23
+ * or check (AGL-3080) — an automation step choosing the dataset it writes to,
24
+ * a reference check confirming one still exists.
25
+ *
26
+ * Narrowed to a site (`hostId`), the query asks only for the datasets shared
27
+ * with it, by the site's scope tokens: an automation runs on the site, so it
28
+ * may only reach datasets the site can use, and the filter is the one the
29
+ * security rules require of a member scoped to the site. With no site, a
30
+ * member who is not organization-wide (`memberScope`) is asked for the ones
31
+ * shared with any site they reach — the rules refuse them an unfiltered
32
+ * list — and an organization-wide reader lists the organization's own.
33
+ * `installedFrom` keeps the ones installed from that listing, by the
34
+ * `source.listingId` every dataset install stamps.
35
+ *
36
+ * Ordered by document id, which the automatic index serves beside the
37
+ * `visibleTo` filter, and the composite index on (`source.listingId`,
38
+ * `visibleTo`) beside both. Each record is `datasetIndexedRecord`'s, the
39
+ * server index's shape.
40
+ */ export const datasetRecordListSource = {
41
+ query (firestore, request) {
42
+ var _request_memberScope;
43
+ if (!request.orgId) return null;
44
+ const scope = request.hostId ? scopeTokensForHost(request.hostId) : (_request_memberScope = request.memberScope) != null ? _request_memberScope : null;
45
+ return query(collection(firestore, 'orgs', request.orgId, 'datasets'), ...request.installedFrom ? [
46
+ where('source.listingId', '==', request.installedFrom)
47
+ ] : [], ...scope ? [
48
+ where('visibleTo', 'array-contains-any', [
49
+ ...scope
50
+ ])
51
+ ] : [], orderBy(documentId()), limit(request.limit));
52
+ },
53
+ record: datasetIndexedRecord
54
+ };
55
+ /** Called from the console registrar, owner named for a spec that calls it directly. */ export function registerDatasetRecordList() {
56
+ registerPluginRecordListSource('dataset', datasetRecordListSource, {
57
+ pluginId: BUNDLE_ID
58
+ });
59
+ }
60
+
61
+ //# sourceMappingURL=dataset-record-list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/dataset-record-list.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 { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n registerPluginRecordListSource,\n type PluginRecordListSource,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-lists'\nimport {\n collection,\n documentId,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { datasetIndexedRecord } from './dataset-record'\n\n/**\n * The workspace's datasets, listed in the console for another plugin's picker\n * or check (AGL-3080) — an automation step choosing the dataset it writes to,\n * a reference check confirming one still exists.\n *\n * Narrowed to a site (`hostId`), the query asks only for the datasets shared\n * with it, by the site's scope tokens: an automation runs on the site, so it\n * may only reach datasets the site can use, and the filter is the one the\n * security rules require of a member scoped to the site. With no site, a\n * member who is not organization-wide (`memberScope`) is asked for the ones\n * shared with any site they reach — the rules refuse them an unfiltered\n * list — and an organization-wide reader lists the organization's own.\n * `installedFrom` keeps the ones installed from that listing, by the\n * `source.listingId` every dataset install stamps.\n *\n * Ordered by document id, which the automatic index serves beside the\n * `visibleTo` filter, and the composite index on (`source.listingId`,\n * `visibleTo`) beside both. Each record is `datasetIndexedRecord`'s, the\n * server index's shape.\n */\nexport const datasetRecordListSource: PluginRecordListSource = {\n query(firestore, request) {\n if (!request.orgId) return null\n const scope = request.hostId ? scopeTokensForHost(request.hostId) : (request.memberScope ?? null)\n return query(\n collection(firestore, 'orgs', request.orgId, 'datasets'),\n ...(request.installedFrom ? [where('source.listingId', '==', request.installedFrom)] : []),\n ...(scope ? [where('visibleTo', 'array-contains-any', [...scope])] : []),\n orderBy(documentId()),\n limit(request.limit),\n )\n },\n record: datasetIndexedRecord,\n}\n\n/** Called from the console registrar, owner named for a spec that calls it directly. */\nexport function registerDatasetRecordList(): void {\n registerPluginRecordListSource('dataset', datasetRecordListSource, { pluginId: BUNDLE_ID })\n}\n"],"names":["scopeTokensForHost","registerPluginRecordListSource","collection","documentId","limit","orderBy","query","where","BUNDLE_ID","datasetIndexedRecord","datasetRecordListSource","firestore","request","orgId","scope","hostId","memberScope","installedFrom","record","registerDatasetRecordList","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,sCAAqC;AACxE,SACEC,8BAA8B,QAEzB,kDAAiD;AACxD,SACEC,UAAU,EACVC,UAAU,EACVC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,KAAK,QACA,qBAAoB;AAC3B,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,oBAAoB,QAAQ,sBAAkB;AAEvD;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,MAAMC,0BAAkD;IAC7DJ,OAAMK,SAAS,EAAEC,OAAO;YAE+CA;QADrE,IAAI,CAACA,QAAQC,KAAK,EAAE,OAAO;QAC3B,MAAMC,QAAQF,QAAQG,MAAM,GAAGf,mBAAmBY,QAAQG,MAAM,KAAKH,uBAAAA,QAAQI,WAAW,YAAnBJ,uBAAuB;QAC5F,OAAON,MACLJ,WAAWS,WAAW,QAAQC,QAAQC,KAAK,EAAE,gBACzCD,QAAQK,aAAa,GAAG;YAACV,MAAM,oBAAoB,MAAMK,QAAQK,aAAa;SAAE,GAAG,EAAE,KACrFH,QAAQ;YAACP,MAAM,aAAa,sBAAsB;mBAAIO;aAAM;SAAE,GAAG,EAAE,EACvET,QAAQF,eACRC,MAAMQ,QAAQR,KAAK;IAEvB;IACAc,QAAQT;AACV,EAAC;AAED,sFAAsF,GACtF,OAAO,SAASU;IACdlB,+BAA+B,WAAWS,yBAAyB;QAAEU,UAAUZ;IAAU;AAC3F"}
@@ -0,0 +1,33 @@
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 { PluginIndexedRecord } from '@aglyn/aglyn/plugin-manager/plugin-record-index';
18
+ /**
19
+ * One stored dataset as this plugin shares it with another (AGL-3080) — the
20
+ * one shape its server index (`dataset-record-index.ts`) and its console list
21
+ * source (`dataset-record-list.ts`) both answer, so a reader on either side
22
+ * reads the same record.
23
+ *
24
+ * `null` for a deleted dataset. Otherwise its id, its name as the Data page
25
+ * shows it, and `facts`:
26
+ * `fields` — its fields in the dataset page's order, each
27
+ * `{ id, name, type }`; `visibleTo` — the scope tokens it is shared
28
+ * with, so a reader acting for a member can apply the platform's own
29
+ * visibility rule to them; and `installedFrom` — `{ listingId, version }`
30
+ * for a dataset installed from a listing (`version` as the install
31
+ * stamped it), `null` for one the workspace made. Never a record of it.
32
+ */
33
+ export declare function datasetIndexedRecord(id: string, data: Readonly<Record<string, unknown>> | undefined): PluginIndexedRecord | null;
@@ -0,0 +1,68 @@
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 } from "@aglyn/aglyn/app-utils/dataset-models";
17
+ import { datasetDisplayName } from "@aglyn/aglyn/app-utils/datasets";
18
+ /**
19
+ * One stored dataset as this plugin shares it with another (AGL-3080) — the
20
+ * one shape its server index (`dataset-record-index.ts`) and its console list
21
+ * source (`dataset-record-list.ts`) both answer, so a reader on either side
22
+ * reads the same record.
23
+ *
24
+ * `null` for a deleted dataset. Otherwise its id, its name as the Data page
25
+ * shows it, and `facts`:
26
+ * `fields` — its fields in the dataset page's order, each
27
+ * `{ id, name, type }`; `visibleTo` — the scope tokens it is shared
28
+ * with, so a reader acting for a member can apply the platform's own
29
+ * visibility rule to them; and `installedFrom` — `{ listingId, version }`
30
+ * for a dataset installed from a listing (`version` as the install
31
+ * stamped it), `null` for one the workspace made. Never a record of it.
32
+ */ export function datasetIndexedRecord(id, data) {
33
+ if (!data || data['deletedAt'] != null) return null;
34
+ const model = effectiveDatasetModel(data);
35
+ const text = (value)=>typeof value === 'string' ? value.trim() : '';
36
+ return {
37
+ id,
38
+ name: datasetDisplayName(data) || id,
39
+ facts: {
40
+ fields: model.order.map((fieldId)=>{
41
+ var _ref;
42
+ var _model_fields_fieldId, _model_fields_fieldId1;
43
+ return {
44
+ id: fieldId,
45
+ name: text((_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId.name) || fieldId,
46
+ type: String((_ref = (_model_fields_fieldId1 = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId1.type) != null ? _ref : 'text')
47
+ };
48
+ }),
49
+ visibleTo: Array.isArray(data['visibleTo']) ? data['visibleTo'] : [],
50
+ installedFrom: installedFrom(data['source'])
51
+ }
52
+ };
53
+ }
54
+ /**
55
+ * Where an installed dataset came from. Read from its `source` stamp, which
56
+ * every dataset install has written — the ones from before install
57
+ * provenance (AGL-1015) included, which carry no `installedFrom`.
58
+ */ function installedFrom(source) {
59
+ var _stamp_version;
60
+ const stamp = source != null ? source : {};
61
+ const listingId = typeof stamp['listingId'] === 'string' ? stamp['listingId'] : '';
62
+ return listingId ? {
63
+ listingId,
64
+ version: (_stamp_version = stamp['version']) != null ? _stamp_version : null
65
+ } : null;
66
+ }
67
+
68
+ //# sourceMappingURL=dataset-record.js.map
@@ -0,0 +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 { effectiveDatasetModel, type DatasetModel } from '@aglyn/aglyn/app-utils/dataset-models'\nimport { datasetDisplayName } from '@aglyn/aglyn/app-utils/datasets'\nimport type { PluginIndexedRecord } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\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,GAED,SAASA,qBAAqB,QAA2B,wCAAuC;AAChG,SAASC,kBAAkB,QAAQ,kCAAiC;AAGpE;;;;;;;;;;;;;;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;uBAHuB;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;gBAC9C;;YACAE,WAAWC,MAAMC,OAAO,CAACf,IAAI,CAAC,YAAY,IAAKA,IAAI,CAAC,YAAY,GAAgB,EAAE;YAClFgB,eAAeA,cAAchB,IAAI,CAAC,SAAS;QAC7C;IACF;AACF;AAEA;;;;CAIC,GACD,SAASgB,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
@@ -19,6 +19,7 @@ import { registerRepeatSource } from "@aglyn/aglyn/app-utils/repeat-sources";
19
19
  import { mdiDatabaseOutline } from "@aglyn/shared-data-mdi";
20
20
  import { lazy } from "react";
21
21
  import { BUNDLE_ID } from "./constants/bundle-common.js";
22
+ import { registerDatasetRecordList } from "./model/dataset-record-list.js";
22
23
  import { DATASET_REPEAT_SOURCE } from "./repeat/dataset-repeat-source.js";
23
24
  /** Code-split: the Data console page only loads when opened. */ const DataConsolePage = lazy(()=>import("./components/data-console-page.js"));
24
25
  /**
@@ -38,6 +39,8 @@ import { DATASET_REPEAT_SOURCE } from "./repeat/dataset-repeat-source.js";
38
39
  * imported only to run one (AGL-3025).
39
40
  */ export function registerDataConsole() {
40
41
  registerRepeatSource(DATASET_REPEAT_SOURCE);
42
+ // The workspace's datasets, for another plugin's picker (AGL-3080).
43
+ registerDatasetRecordList();
41
44
  Aglyn.registerConsoleExtension({
42
45
  // Org datasets card (AGL-419): org/data renders it through the
43
46
  // 'orgData' widget slot.
@@ -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 { DATASET_REPEAT_SOURCE } from './repeat/dataset-repeat-source'\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 * 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 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 ],\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","DATASET_REPEAT_SOURCE","DataConsolePage","registerDataConsole","registerConsoleExtension","widgets","slot","widgetId","Component","pluginId","displayName","featureFlag","navItems","label","href","navTabId","icon","path","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,qBAAqB,QAAQ,oCAAgC;AAEtE,8DAA8D,GAC9D,MAAMC,kBAAkBH,KAAK,IAAM,MAAM,CAAC;AAE1C;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASI;IACdN,qBAAqBI;IACrBL,MAAMQ,wBAAwB,CAAC;QAC7B,+DAA+D;QAC/D,yBAAyB;QACzBC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,WAAWb;YACb;SACD;QACDc,UAAUT;QACVU,aAAa;QACbC,aAAa;QACbC,UAAU;YACR;gBACEC,OAAO;gBACPC,MAAM;gBACN,qEAAqE;gBACrE,8DAA8D;gBAC9DC,UAAU;gBACVC,MAAM;oBAAEC,MAAMnB,mBAAmBmB,IAAI;gBAAC;gBACtCC,QAAQ;oBACNC,OAAO;oBACPH,MAAM;wBAAEC,MAAMnB,mBAAmBmB,IAAI;oBAAC;oBACtCG,WAAW;gBACb;gBACAZ,WAAWN;YACb;SACD;IACH;AACF;AAEA,SAASmB,WAAW1B,gBAAgB,QAAQ,+CAA2C"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/data/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport HostDatasetsCard from './components/host-datasets-card.component'\nimport * as Aglyn from '@aglyn/aglyn'\nimport { registerRepeatSource } from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { mdiDatabaseOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerDatasetRecordList } from './model/dataset-record-list'\nimport { DATASET_REPEAT_SOURCE } from './repeat/dataset-repeat-source'\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 * 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 workspace's datasets, for another plugin's picker (AGL-3080).\n registerDatasetRecordList()\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 ],\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","DATASET_REPEAT_SOURCE","DataConsolePage","registerDataConsole","registerConsoleExtension","widgets","slot","widgetId","Component","pluginId","displayName","featureFlag","navItems","label","href","navTabId","icon","path","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,qBAAqB,QAAQ,oCAAgC;AAEtE,8DAA8D,GAC9D,MAAMC,kBAAkBJ,KAAK,IAAM,MAAM,CAAC;AAE1C;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASK;IACdP,qBAAqBK;IACrB,oEAAoE;IACpED;IACAL,MAAMS,wBAAwB,CAAC;QAC7B,+DAA+D;QAC/D,yBAAyB;QACzBC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,WAAWd;YACb;SACD;QACDe,UAAUV;QACVW,aAAa;QACbC,aAAa;QACbC,UAAU;YACR;gBACEC,OAAO;gBACPC,MAAM;gBACN,qEAAqE;gBACrE,8DAA8D;gBAC9DC,UAAU;gBACVC,MAAM;oBAAEC,MAAMpB,mBAAmBoB,IAAI;gBAAC;gBACtCC,QAAQ;oBACNC,OAAO;oBACPH,MAAM;wBAAEC,MAAMpB,mBAAmBoB,IAAI;oBAAC;oBACtCG,WAAW;gBACb;gBACAZ,WAAWN;YACb;SACD;IACH;AACF;AAEA,SAASmB,WAAW3B,gBAAgB,QAAQ,+CAA2C"}
@@ -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":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,qBAAqB,EAGrBC,kBAAkB,EAElBC,aAAa,QACR,sBAAqB;AAK5B,SAASC,mBAAmB,QAAQ,2BAA0B;AAC9D,SACEC,+BAA+B,EAC/BC,aAAa,EACbC,eAAe,QACV,wCAAuC;AAC9C,SAASC,sBAAsB,QAAQ,4CAA2C;AAClF,SAASC,SAAS,QAAQ,2BAA0B;AAEpD;;;;;CAKC,GACD,MAAMC,uBAAuBL;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GACD,OAAO,eAAeM,yBACpBC,OAA0B;IAE1B,MAAMC,OAAO;WACR,IAAIC,IAAIF,QAAQC,IAAI,CAACE,GAAG,CAAC,CAACC,MAAQA,IAAIC,IAAI,IAAIC,MAAM,CAACC;KACzD,CAACC,IAAI;IACN,IAAI,CAACP,KAAKQ,MAAM,EAAE,OAAO,CAAC;IAC1B,IAAI;QACF,OAAO,MAAMd,gBAAgB;YAC3BS,KAAK;gBAAC;gBAAmBJ,QAAQU,MAAM;mBAAKT;aAAK;YACjDU,YAAYb;YACZc,MAAM;gBAAClB,cAAcM,QAAQU,MAAM;aAAE;YACrCG,MAAM,IAAMC,aAAad,QAAQU,MAAM,EAAET;QAC3C;IACF,EAAE,OAAOc,OAAO;QACdC,QAAQD,KAAK,CAACA;QACd,OAAOD,aAAad,QAAQU,MAAM,EAAET;IACtC;AACF;AAEA,eAAea,aACbJ,MAAc,EACdT,IAAuB;IAEvB,MAAMgB,WAA8C,CAAC;IACrD,IAAI;YAIgBC,oBAAAA;QAHlB,MAAM,EAAEA,GAAG,EAAEC,KAAK,EAAE,GAAG,MAAM3B,oBAAoBkB,QAAQ;QACzD,qEAAqE;QACrE,sEAAsE;QACtE,MAAMU,YAAYF,EAAAA,cAAAA,IAAIG,MAAM,sBAAVH,qBAAAA,YAAYG,MAAM,qBAAlBH,mBAAoBI,EAAE,MAAK;QAC7C,MAAMC,SAAS,CAACC,WACdjB,QAAQiB,4BAAAA,SAAUC,MAAM,KACxB,EAACD,4BAAAA,SAAUE,GAAG,CAAC,iBACd,CAAA,CAACN,aAAa7B,cAAciC,4BAAAA,SAAUE,GAAG,CAAC,cAAchB,OAAM;QAEjE,MAAMiB,QAAQ,IAAIC;QAClB,MAAMC,OAAO,CAACL;YACZ,IAAIM,UAAUH,MAAMD,GAAG,CAACF,SAASF,EAAE;YACnC,IAAI,CAACQ,SAAS;gBACZA,UAAUC,kBAAkBP,SAASN,GAAG,EAAEc,IAAI,CAAC,CAACC,UAAa,CAAA;wBAC3DA;wBACAC,OAAO7C,sBACLmC,SAASW,IAAI;oBAEjB,CAAA;gBACAR,MAAMS,GAAG,CAACZ,SAASF,EAAE,EAAEQ;YACzB;YACA,OAAOA;QACT;QAEA,yEAAyE;QACzE,yEAAyE;QACzE,oEAAoE;QACpE,MAAMO,aAAa,OAAOjC;YACxB,IAAI,CAACA,IAAIkC,QAAQ,CAAC,MAAM;gBACtB,MAAMC,OAAO,MAAMrB,IAAIsB,GAAG,CAACpC,KAAKsB,GAAG;gBACnC,IAAIa,KAAKd,MAAM,EAAE,OAAOF,OAAOgB,QAAQA,OAAOE;YAChD;YACA,MAAMC,SAAS,MAAMvB,MAAMwB,KAAK,CAAC,eAAe,MAAMvC,KAAKwC,KAAK,CAAC,GAAGlB,GAAG;YACvE,OAAOgB,OAAOG,IAAI,CAACC,IAAI,CAAC,CAACtB,WAAaD,OAAOC;QAC/C;QAEA,MAAMuB,WAAW,MAAMC,QAAQC,GAAG,CAChChD,KAAKE,GAAG,CAAC,OAAOC;YACd,IAAI;gBACF,MAAMoB,WAAW,MAAMa,WAAWjC;gBAClC,IAAI,CAACoB,UAAU,OAAOiB;gBACtB,MAAMX,UAAU,MAAMD,KAAKL;gBAC3BP,QAAQ,CAACb,IAAI,GAAG0B;gBAChBb,QAAQ,CAACO,SAASF,EAAE,CAAC,GAAGQ;gBACxB,OAAOA;YACT,EAAE,OAAOf,OAAO;gBACdC,QAAQD,KAAK,CAACA;gBACd,OAAO0B;YACT;QACF;QAGF,yEAAyE;QACzE,6EAA6E;QAC7E,MAAMS,UAAU,IAAIhD;QACpB,KAAK,MAAM4B,WAAWiB,SAAU;;gBACRjB;YAAtB,KAAK,MAAMqB,mBAAWrB,4BAAAA,iBAAAA,QAASI,KAAK,qBAAdJ,eAAgBsB,KAAK,mBAAI,EAAE,CAAE;oBACnCtB,iBAEkBuB;gBAFhC,MAAMA,QAAQvB,4BAAAA,kBAAAA,QAASI,KAAK,qBAAdJ,gBAAgBwB,MAAM,CAACH,QAAQ;gBAC7C,MAAMI,WACJF,CAAAA,yBAAAA,MAAOG,IAAI,MAAK,eAAcH,mBAAAA,MAAMI,SAAS,qBAAfJ,iBAAiBK,SAAS,GAAGjB;gBAC7D,IAAIc,YAAY,CAACA,SAASjB,QAAQ,CAAC,QAAQ,CAACrB,QAAQ,CAACsC,SAAS,EAAE;oBAC9DL,QAAQS,GAAG,CAACJ;gBACd;YACF;QACF;QACA,MAAMP,QAAQC,GAAG,CACf;eAAIC;SAAQ,CAAC/C,GAAG,CAAC,OAAOoD;YACtB,IAAI;gBACF,MAAMK,SAAS,MAAM1C,IAAIsB,GAAG,CAACe,UAAU7B,GAAG;gBAC1C,IAAIH,OAAOqC,SAAS3C,QAAQ,CAACsC,SAAS,GAAG,MAAM1B,KAAK+B;YACtD,EAAE,OAAO7C,OAAO;gBACdC,QAAQD,KAAK,CAACA;YAChB;QACF;IAEJ,EAAE,OAAOA,OAAO;QACdC,QAAQD,KAAK,CAACA;IAChB;IACA,OAAOE;AACT;AAEA;;;;;;;;;;CAUC,GACD,eAAec,kBACb8B,UAA+C;IAE/C,MAAMC,aAAaD,WAAWE,UAAU,CAAC;IACzC,MAAMC,OAAO,CAACxC,WACZA,SAASqB,IAAI,CAAC1C,GAAG,CAAC,CAACqC,MAAS,CAAA;gBAC1BlB,IAAIkB,IAAIlB,EAAE;gBACVa,MAAMK,IAAIL,IAAI;YAChB,CAAA;IACF,MAAM8B,UAAU,MAAMH,WACnBI,OAAO,CAAC,SACRtB,KAAK,CAACtD,oBACNoC,GAAG;IACN,MAAMa,OACJ0B,QAAQpB,IAAI,CAACpC,MAAM,GAAGnB,qBAClB,MAAMwE,WACHI,OAAO,CAACrE,UAAUsE,UAAU,IAC5BvB,KAAK,CAACtD,oBACNoC,GAAG,KACNe;IACN,OAAO7C,uBAAuBoE,KAAKC,UAAU1B,OAAOyB,KAAKzB,QAAQ,EAAE;AACrE"}
@@ -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',