@aglyn/plugins-data 1.0.0-beta.221 → 1.0.0-beta.223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/package.json +10 -10
- package/src/index.d.ts +1 -1
- package/src/index.js +1 -1
- package/src/index.js.map +1 -1
- package/src/lib/artifact/dataset-schema-artifact.server.d.ts +57 -0
- package/src/lib/artifact/dataset-schema-artifact.server.js +308 -0
- package/src/lib/artifact/dataset-schema-artifact.server.js.map +1 -0
- package/src/lib/artifact/dataset-schema.d.ts +136 -0
- package/src/lib/artifact/dataset-schema.js +232 -0
- package/src/lib/artifact/dataset-schema.js.map +1 -0
- package/src/lib/components/dataset-record-dialog.component.d.ts +1 -1
- package/src/lib/components/dataset-record-dialog.component.js +1 -1
- package/src/lib/components/dataset-record-dialog.component.js.map +1 -1
- package/src/lib/components/dataset-record-filter.d.ts +1 -1
- package/src/lib/components/dataset-record-filter.js +2 -2
- package/src/lib/components/dataset-record-filter.js.map +1 -1
- package/src/lib/components/dataset-schema-dialog.component.d.ts +1 -1
- package/src/lib/components/dataset-schema-dialog.component.js +55 -23
- package/src/lib/components/dataset-schema-dialog.component.js.map +1 -1
- package/src/lib/components/host-datasets-card.component.js +91 -403
- package/src/lib/components/host-datasets-card.component.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +12 -0
- package/src/lib/constants/bundle-common.js +13 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +16 -1
- package/src/lib/declarations.console-server.js +34 -2
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.server.d.ts +13 -0
- package/src/lib/declarations.server.js +48 -2
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/form-target/dataset-form-record-target.server.js +5 -3
- package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -1
- package/src/lib/model/dataset-models.d.ts +392 -0
- package/src/lib/model/dataset-models.js +806 -0
- package/src/lib/model/dataset-models.js.map +1 -0
- package/src/lib/model/dataset-record-view.d.ts +1 -1
- package/src/lib/model/dataset-record-view.js +1 -2
- package/src/lib/model/dataset-record-view.js.map +1 -1
- package/src/lib/model/dataset-record.js +2 -2
- package/src/lib/model/dataset-record.js.map +1 -1
- package/src/lib/model/datasets.d.ts +162 -0
- package/src/lib/model/datasets.js +227 -0
- package/src/lib/model/datasets.js.map +1 -0
- package/src/lib/plugin.d.ts +4 -0
- package/src/lib/plugin.js +40 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/record-pages/record-page-live-paths.server.d.ts +55 -0
- package/src/lib/record-pages/record-page-live-paths.server.js +83 -0
- package/src/lib/record-pages/record-page-live-paths.server.js.map +1 -0
- package/src/lib/record-pages/record-page-read.server.d.ts +70 -0
- package/src/lib/record-pages/record-page-read.server.js +175 -0
- package/src/lib/record-pages/record-page-read.server.js.map +1 -0
- package/src/lib/record-pages/record-page-resolver.server.d.ts +37 -0
- package/src/lib/record-pages/record-page-resolver.server.js +131 -0
- package/src/lib/record-pages/record-page-resolver.server.js.map +1 -0
- package/src/lib/record-pages/record-page-sitemap.server.d.ts +32 -0
- package/src/lib/record-pages/record-page-sitemap.server.js +102 -0
- package/src/lib/record-pages/record-page-sitemap.server.js.map +1 -0
- package/src/lib/record-pages/record-page-source.d.ts +42 -0
- package/src/lib/record-pages/record-page-source.js +176 -0
- package/src/lib/record-pages/record-page-source.js.map +1 -0
- package/src/lib/record-pages/record-pages-route.d.ts +42 -0
- package/src/lib/record-pages/record-pages-route.js +348 -0
- package/src/lib/record-pages/record-pages-route.js.map +1 -0
- package/src/lib/record-pages/record-pages.d.ts +218 -0
- package/src/lib/record-pages/record-pages.js +351 -0
- package/src/lib/record-pages/record-pages.js.map +1 -0
- package/src/lib/record-pages/record-template-row-chip.component.d.ts +28 -0
- package/src/lib/record-pages/record-template-row-chip.component.js +87 -0
- package/src/lib/record-pages/record-template-row-chip.component.js.map +1 -0
- package/src/lib/record-pages/record-template-section.component.d.ts +30 -0
- package/src/lib/record-pages/record-template-section.component.js +491 -0
- package/src/lib/record-pages/record-template-section.component.js.map +1 -0
- package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -1
- package/src/lib/repeat/dataset-repeat-rows.js +30 -12
- package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
- package/src/lib/repeat/dataset-repeat-rows.server.d.ts +1 -1
- package/src/lib/repeat/dataset-repeat-rows.server.js +23 -12
- package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -1
- package/src/lib/repeat/repeat-record-pages.d.ts +48 -0
- package/src/lib/repeat/repeat-record-pages.js +64 -0
- package/src/lib/repeat/repeat-record-pages.js.map +1 -0
- package/src/lib/server/announce-dataset-records.d.ts +1 -1
- package/src/lib/server/announce-dataset-records.js +1 -1
- package/src/lib/server/announce-dataset-records.js.map +1 -1
- package/src/lib/server/api-v1/datasets.js +5 -3
- package/src/lib/server/api-v1/datasets.js.map +1 -1
- package/src/lib/server/dataset-figures.js +2 -2
- package/src/lib/server/dataset-figures.js.map +1 -1
- package/src/lib/server/dataset-live-pages.d.ts +147 -0
- package/src/lib/server/dataset-live-pages.js +436 -0
- package/src/lib/server/dataset-live-pages.js.map +1 -0
- package/src/lib/server/dataset-steps.server.d.ts +45 -0
- package/src/lib/server/dataset-steps.server.js +268 -0
- package/src/lib/server/dataset-steps.server.js.map +1 -0
- package/src/lib/server/datasets-route.d.ts +2 -0
- package/src/lib/server/datasets-route.js +102 -6
- package/src/lib/server/datasets-route.js.map +1 -1
- package/src/lib/server/resolve-dataset.d.ts +40 -0
- package/src/lib/server/resolve-dataset.js +64 -0
- package/src/lib/server/resolve-dataset.js.map +1 -0
- package/src/lib/server.d.ts +3 -2
- package/src/lib/server.js +8 -9
- package/src/lib/server.js.map +1 -1
- package/src/lib/site-bundle/datasets-package.d.ts +42 -0
- package/src/lib/site-bundle/datasets-package.js +95 -0
- package/src/lib/site-bundle/datasets-package.js.map +1 -0
- package/src/lib/site-bundle/datasets-site-bundle.server.js +2 -2
- package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -1
- package/src/lib/{model/index.js → tenant-api.d.ts} +12 -7
- package/src/lib/tenant-api.js +30 -0
- package/src/lib/tenant-api.js.map +1 -0
- package/src/lib/transfer/dataset-transfer-key.d.ts +30 -0
- package/src/lib/transfer/dataset-transfer-key.js +31 -0
- package/src/lib/transfer/dataset-transfer-key.js.map +1 -0
- package/src/lib/transfer/dataset-transfer-model.d.ts +146 -0
- package/src/lib/transfer/dataset-transfer-model.js +536 -0
- package/src/lib/transfer/dataset-transfer-model.js.map +1 -0
- package/src/lib/{model/index.d.ts → transfer/dataset-transfer-resource.d.ts} +2 -6
- package/src/lib/transfer/dataset-transfer-resource.js +41 -0
- package/src/lib/transfer/dataset-transfer-resource.js.map +1 -0
- package/src/lib/transfer/dataset-transfer.server.d.ts +69 -0
- package/src/lib/transfer/dataset-transfer.server.js +930 -0
- package/src/lib/transfer/dataset-transfer.server.js.map +1 -0
- package/src/lib/model/dataset-csv.d.ts +0 -36
- package/src/lib/model/dataset-csv.js +0 -85
- package/src/lib/model/dataset-csv.js.map +0 -1
- package/src/lib/model/dataset-io.d.ts +0 -44
- package/src/lib/model/dataset-io.js +0 -85
- package/src/lib/model/dataset-io.js.map +0 -1
- package/src/lib/model/index.js.map +0 -1
- package/src/lib/server/datasets-export-route.d.ts +0 -57
- package/src/lib/server/datasets-export-route.js +0 -229
- package/src/lib/server/datasets-export-route.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/record-pages/record-template-section.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { checkEntitlement, scopeTokensForHost } from '@aglyn/aglyn'\nimport type { ConsoleBesignerPagePropertiesZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n useFirestore,\n useFirestoreCollection,\n useOrgDataScope,\n useOrgPlan,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n InputAdornment,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection, limit, query, where } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { type DatasetModel, effectiveDatasetModel } from '../model/dataset-models'\nimport { type HostDataset, datasetDisplayName } from '../model/datasets'\nimport {\n type DatasetRecordPageBinding,\n isRecordAddressField,\n recordAddressFieldIds,\n recordPageBaseRefusal,\n recordPagePath,\n} from './record-pages'\nimport { useRecordPagePreview, useSiteRecordPageBindings } from './record-page-source'\n\ntype DatasetDoc = HostDataset & { $id: string; deletedAt?: unknown }\n\n/** The optional field pickers, in the order the section offers them. */\nconst FIELD_PICKERS = [\n {\n key: 'titleField',\n label: 'Page name from',\n helper: 'Names each page — its tab title and breadcrumb. Defaults to the first text field.',\n empty: 'First text field',\n },\n {\n key: 'seoTitleField',\n label: 'Search title from',\n helper: 'Used exactly as written. Without one, the page name and the site title are used.',\n empty: 'Page name',\n },\n {\n key: 'seoDescriptionField',\n label: 'Search description from',\n helper: 'Without one, this page’s own description is used.',\n empty: 'This page’s description',\n },\n {\n key: 'seoImageField',\n label: 'Sharing image from',\n helper: 'A media field or an image URL. Without one, this page’s image is used.',\n empty: 'This page’s image',\n },\n] as const\n\ntype Draft = Omit<DatasetRecordPageBinding, 'screenId'>\n\nconst EMPTY_DRAFT: Draft = { datasetId: '', base: '', slugField: '' }\n\n/**\n * Page Properties' \"Record pages\" section (AGL-3475): makes the page in the\n * editor the record template of a dataset, so it is served once per record at\n * `/{base}/{address}`, and picks the record the canvas draws it for.\n *\n * Saving a page that is not yet a template converts it first, through the\n * platform's own route, after saying what that means: it stops answering at\n * its own address and stops counting toward the plan's pages. The binding is\n * then written by the record-pages route, which re-checks everything this\n * section checks and the few things only the server can see.\n */\nexport function RecordTemplateSection(props: ConsoleBesignerPagePropertiesZoneProps) {\n const { hostId, orgId, screenId, screenKind } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const { org, ready: planReady } = useOrgPlan(hostId)\n const { scope } = useOrgDataScope({ hostId, orgId })\n const { bindings } = useSiteRecordPageBindings(hostId)\n const current = bindings.find((binding) => binding.screenId === screenId)\n const preview = useRecordPagePreview({ hostId, screenId })\n\n // The datasets THIS site may see, which are the ones a record page can\n // render: the same scope the published page's reads apply.\n const { data: datasetDocs } = useFirestoreCollection<DatasetDoc>(\n () =>\n scope\n ? query(\n collection(firestore, scope[0], scope[1], 'datasets'),\n where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)),\n limit(100),\n )\n : null,\n [firestore, scope, hostId],\n { idField: '$id' },\n )\n const datasets = useMemo(\n () =>\n (datasetDocs ?? [])\n .filter((dataset) => !dataset.deletedAt)\n .sort((a, b) => datasetDisplayName(a).localeCompare(datasetDisplayName(b))),\n [datasetDocs],\n )\n\n const [draft, setDraft] = useState<Draft>(EMPTY_DRAFT)\n const [sourceField, setSourceField] = useState('')\n const [busy, setBusy] = useState(false)\n useEffect(() => {\n if (current) {\n const { screenId: _id, ...stored } = current\n setDraft(stored)\n } else {\n setDraft(EMPTY_DRAFT)\n }\n }, [current])\n\n const dataset = datasets.find((one) => one.$id === draft.datasetId)\n const model: DatasetModel | undefined = dataset ? effectiveDatasetModel(dataset) : undefined\n const addressFields = model ? recordAddressFieldIds(model) : []\n const textFields = model\n ? model.order.filter(\n (fieldId) =>\n model.fields[fieldId]?.type === 'text' && !isRecordAddressField(model.fields[fieldId]),\n )\n : []\n const baseRefusal = draft.base\n ? recordPageBaseRefusal(draft.base, {\n otherBases: bindings\n .filter((binding) => binding.screenId !== screenId)\n .map((binding) => binding.base),\n })\n : null\n\n // A dataset with exactly one address field needs no choice.\n useEffect(() => {\n if (model && !draft.slugField && addressFields.length === 1) {\n setDraft((previous) => ({ ...previous, slugField: addressFields[0] }))\n }\n }, [model, draft.slugField, addressFields])\n\n const post = useCallback(\n async (path: string, body: Record<string, unknown>) => {\n const response = await authorizedFetch(user, path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n const result = (await response.json().catch(() => ({}))) as Record<string, any>\n if (!response.ok) throw new Error(result?.['error'] ?? 'Something went wrong')\n return result\n },\n [user],\n )\n\n const handleAddAddresses = useCallback(async () => {\n if (!dataset || !sourceField || !orgId) return\n setBusy(true)\n try {\n const result = await post('/api/orgs/datasets', {\n orgId,\n action: 'add-address-field',\n datasetId: dataset.$id,\n sourceField,\n })\n setDraft((previous) => ({ ...previous, slugField: String(result['fieldId'] ?? '') }))\n enqueueSnackbar(\n result['truncated']\n ? `Gave ${result['filled']} records a page address — run it again for the rest`\n : `Gave ${result['filled']} records a page address`,\n { variant: 'success' },\n )\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error' })\n } finally {\n setBusy(false)\n }\n }, [dataset, sourceField, orgId, post, enqueueSnackbar])\n\n const handleSave = useCallback(async () => {\n if (screenKind !== 'template') {\n const accepted = await confirm({\n title: 'Make this page a record template?',\n description:\n `Each record gets its own page at ${recordPagePath(draft.base || 'base', 'address')}, ` +\n 'drawn by this design. This page stops answering at its own address and no ' +\n 'longer counts toward your plan’s pages.',\n confirmationText: 'Make it a template',\n })\n // The dialog rejects on cancel.\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n }\n setBusy(true)\n try {\n if (screenKind !== 'template') {\n await post('/api/hosts/screens', {\n action: 'convert',\n hostId,\n id: screenId,\n kind: 'template',\n })\n }\n await post('/api/hosts/record-pages', { action: 'save', hostId, screenId, ...draft })\n enqueueSnackbar('Record pages saved', { variant: 'success' })\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error' })\n } finally {\n setBusy(false)\n }\n }, [screenKind, confirm, draft, post, hostId, screenId, enqueueSnackbar])\n\n const handleRemove = useCallback(async () => {\n const accepted = await confirm({\n title: 'Stop serving record pages?',\n description:\n `Every page under /${current?.base ?? ''}/ stops answering. This page stays a ` +\n 'template, so it doesn’t answer at its own address either.',\n confirmationText: 'Stop serving them',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setBusy(true)\n try {\n await post('/api/hosts/record-pages', { action: 'remove', hostId, screenId })\n enqueueSnackbar('Record pages removed', { variant: 'success' })\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error' })\n setBusy(false)\n return\n }\n // The page is left a template, which nobody can visit. Making it a page\n // again is offered once, here, and checked against the plan's pages by\n // the platform's convert route, whose refusal names the count.\n const restore = await confirm({\n title: 'Make this a normal page again?',\n description:\n 'It counts toward your plan’s pages and, if it was published, answers at its ' +\n 'own address again right away. Placeholders like {{item.name}} show as plain ' +\n 'text until you edit them out.',\n confirmationText: 'Make it a page',\n cancellationText: 'Keep it hidden',\n })\n .then(() => true)\n .catch(() => false)\n if (restore) {\n try {\n await post('/api/hosts/screens', { action: 'convert', hostId, id: screenId, kind: 'page' })\n enqueueSnackbar('This is a normal page again', { variant: 'success' })\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error' })\n }\n }\n setBusy(false)\n }, [confirm, current?.base, post, hostId, screenId, enqueueSnackbar])\n\n if (planReady && !checkEntitlement(org as never, 'dataStore')) {\n return (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Record pages'}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Serve this page once per record of a dataset — a page for every service or ' +\n 'location. Record pages need a Starter plan or higher.'}\n </Typography>\n </Stack>\n )\n }\n\n const ready = preview.status === 'ready' ? preview : null\n return (\n <Stack spacing={1.5}>\n <Typography variant=\"subtitle2\">{'Record pages'}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {current\n ? `This page is the template of every record of ${\n dataset ? datasetDisplayName(dataset) : 'its dataset'\n }, each at /${current.base}/ and its page address. Inside it, ` +\n '{{item.field}} fills in from the record.'\n : 'Serve this page once per record of a dataset, each at its own address — ' +\n '/services/roofing. Inside it, {{item.field}} fills in from the record, as in ' +\n 'a repeat.'}\n </Typography>\n <TextField\n select\n size=\"small\"\n label=\"Dataset\"\n value={draft.datasetId}\n onChange={(event) =>\n setDraft({ ...EMPTY_DRAFT, base: draft.base, datasetId: event.target.value })\n }\n helperText=\"Only datasets shared with this site are listed\"\n >\n {datasets.map((one) => (\n <MenuItem key={one.$id} value={one.$id}>\n {datasetDisplayName(one) || one.$id}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Address\"\n value={draft.base}\n onChange={(event) => setDraft({ ...draft, base: event.target.value })}\n error={Boolean(baseRefusal)}\n helperText={\n baseRefusal ??\n (draft.base\n ? `Records are served at /${draft.base.replace(/^\\/+|\\/+$/g, '')}/their-address`\n : 'One or more segments, like services or services/residential')\n }\n slotProps={{\n input: { startAdornment: <InputAdornment position=\"start\">/</InputAdornment> },\n }}\n />\n {model && addressFields.length ? (\n <TextField\n select\n size=\"small\"\n label=\"Page address field\"\n value={draft.slugField}\n onChange={(event) => setDraft({ ...draft, slugField: event.target.value })}\n helperText=\"Each record's own segment of its address\"\n >\n {addressFields.map((fieldId) => (\n <MenuItem key={fieldId} value={fieldId}>\n {model.fields[fieldId]?.name ?? fieldId}\n </MenuItem>\n ))}\n </TextField>\n ) : model ? (\n <Stack spacing={1}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'This dataset has no Page address field yet. Make one from a text field: ' +\n 'every record gets an address made from its value (Roof Repair → ' +\n 'roof-repair), and new records get one as they are added. A record keeps its ' +\n 'address when it is renamed.'}\n </Typography>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <TextField\n select\n size=\"small\"\n label=\"Make addresses from\"\n value={sourceField}\n onChange={(event) => setSourceField(event.target.value)}\n sx={{ flex: 1 }}\n >\n {textFields.map((fieldId) => (\n <MenuItem key={fieldId} value={fieldId}>\n {model.fields[fieldId]?.name ?? fieldId}\n </MenuItem>\n ))}\n </TextField>\n <Button\n size=\"small\"\n variant=\"outlined\"\n onClick={handleAddAddresses}\n disabled={!sourceField || busy}\n >\n {'Make addresses'}\n </Button>\n </Stack>\n </Stack>\n ) : null}\n {model\n ? FIELD_PICKERS.map((picker) => (\n <TextField\n key={picker.key}\n select\n size=\"small\"\n label={picker.label}\n value={draft[picker.key] ?? ''}\n onChange={(event) =>\n setDraft({ ...draft, [picker.key]: event.target.value || undefined })\n }\n helperText={picker.helper}\n // The empty choice is a real one — \"use the default\" — so it is\n // shown rather than leaving the field looking blank.\n slotProps={{ select: { displayEmpty: true }, inputLabel: { shrink: true } }}\n >\n <MenuItem value=\"\">{picker.empty}</MenuItem>\n {model.order.map((fieldId) => (\n <MenuItem key={fieldId} value={fieldId}>\n {model.fields[fieldId]?.name ?? fieldId}\n </MenuItem>\n ))}\n </TextField>\n ))\n : null}\n {ready ? (\n <TextField\n select\n size=\"small\"\n label=\"Preview with\"\n value={ready.selectedId}\n onChange={(event) => ready.select(event.target.value)}\n helperText={`The canvas draws this page for one of ${ready.choices.length}${\n ready.choices.length >= 100 ? '+' : ''\n } records`}\n >\n {ready.choices.map((choice) => (\n <MenuItem key={choice.id} value={choice.id}>\n {choice.label}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"outlined\"\n onClick={handleSave}\n disabled={\n busy || !draft.datasetId || !draft.base || !draft.slugField || Boolean(baseRefusal)\n }\n >\n {current ? 'Save record pages' : 'Serve record pages'}\n </Button>\n {current ? (\n <Button size=\"small\" color=\"error\" onClick={handleRemove} disabled={busy}>\n {'Stop serving'}\n </Button>\n ) : null}\n </Stack>\n </Stack>\n )\n}\n\nexport default RecordTemplateSection\n"],"names":["checkEntitlement","scopeTokensForHost","useConfirmationContext","useSnackbar","authorizedFetch","useFirestore","useFirestoreCollection","useOrgDataScope","useOrgPlan","useUser","Button","InputAdornment","MenuItem","Stack","TextField","Typography","collection","limit","query","where","useCallback","useEffect","useMemo","useState","effectiveDatasetModel","datasetDisplayName","isRecordAddressField","recordAddressFieldIds","recordPageBaseRefusal","recordPagePath","useRecordPagePreview","useSiteRecordPageBindings","FIELD_PICKERS","key","label","helper","empty","EMPTY_DRAFT","datasetId","base","slugField","RecordTemplateSection","props","hostId","orgId","screenId","screenKind","firestore","data","user","enqueueSnackbar","confirm","org","ready","planReady","scope","bindings","current","find","binding","preview","datasetDocs","idField","datasets","filter","dataset","deletedAt","sort","a","b","localeCompare","draft","setDraft","sourceField","setSourceField","busy","setBusy","_id","stored","one","$id","model","undefined","addressFields","textFields","order","fieldId","fields","type","baseRefusal","otherBases","map","length","previous","post","path","body","response","method","headers","JSON","stringify","result","json","catch","ok","Error","handleAddAddresses","action","String","variant","error","message","handleSave","accepted","title","description","confirmationText","then","id","kind","handleRemove","confirmationButtonProps","color","restore","cancellationText","spacing","status","select","size","value","onChange","event","target","helperText","Boolean","replace","slotProps","input","startAdornment","position","name","direction","sx","alignItems","flex","onClick","disabled","picker","displayEmpty","inputLabel","shrink","selectedId","choices","choice"],"mappings":"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"}
|
|
@@ -35,7 +35,8 @@ export interface DatasetRepeatRowsRequest {
|
|
|
35
35
|
* cannot see does not fall through to the name — that would answer "which
|
|
36
36
|
* dataset is called X" for a key that already named a specific one.
|
|
37
37
|
* - A dataset shared with other sites but not this one, or deleted, answers
|
|
38
|
-
* `missing`: the page renders the element
|
|
38
|
+
* `missing`: the page renders no copies of the element (AGL-3496), and
|
|
39
|
+
* the canvas badge says so.
|
|
39
40
|
* - The rows are the two bounded reads and the one ordering rule the page
|
|
40
41
|
* uses, {@link repeatRecordsFromPages}, so a canvas copy is a row the page
|
|
41
42
|
* renders.
|
|
@@ -13,9 +13,12 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
17
|
-
import { repeatRecordsFromPages } from "
|
|
16
|
+
*/ import { REPEAT_MAX_RECORDS, scopeTokensForHost, visibleToHost } from "@aglyn/aglyn";
|
|
17
|
+
import { repeatRecordsFromPages } from "./repeat-record-pages.js";
|
|
18
18
|
import { collection, doc, documentId, getDoc, getDocs, limit, orderBy, query, where } from "firebase/firestore";
|
|
19
|
+
import { effectiveDatasetModel, repeatRowsModelOf } from "../model/dataset-models.js";
|
|
20
|
+
import { datasetDisplayName } from "../model/datasets.js";
|
|
21
|
+
import { DATASET_RECORD_PAGES_COLLECTION, parseRecordPageBinding, withRecordPageUrls } from "../record-pages/record-pages.js";
|
|
19
22
|
/**
|
|
20
23
|
* A dataset's rows for the besigner's canvas preview, read the way the
|
|
21
24
|
* published page's composition reads them (`readPublishedDatasetRows`, beside
|
|
@@ -26,7 +29,8 @@ import { collection, doc, documentId, getDoc, getDocs, limit, orderBy, query, wh
|
|
|
26
29
|
* cannot see does not fall through to the name — that would answer "which
|
|
27
30
|
* dataset is called X" for a key that already named a specific one.
|
|
28
31
|
* - A dataset shared with other sites but not this one, or deleted, answers
|
|
29
|
-
* `missing`: the page renders the element
|
|
32
|
+
* `missing`: the page renders no copies of the element (AGL-3496), and
|
|
33
|
+
* the canvas badge says so.
|
|
30
34
|
* - The rows are the two bounded reads and the one ordering rule the page
|
|
31
35
|
* uses, {@link repeatRecordsFromPages}, so a canvas copy is a row the page
|
|
32
36
|
* renders.
|
|
@@ -58,20 +62,20 @@ import { collection, doc, documentId, getDoc, getDocs, limit, orderBy, query, wh
|
|
|
58
62
|
if (!dataset) return {
|
|
59
63
|
status: 'missing'
|
|
60
64
|
};
|
|
61
|
-
const load = async (snapshot)=>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const load = async (snapshot, page)=>{
|
|
66
|
+
const model = effectiveDatasetModel(snapshot.data());
|
|
67
|
+
return {
|
|
68
|
+
records: withRecordPageUrls(await readRepeatRows(snapshot.ref), page, model),
|
|
69
|
+
model: repeatRowsModelOf(model)
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const rows = await load(dataset, await readRecordPageBinding(firestore, hostId, dataset.id));
|
|
66
73
|
const rowsByKey = {
|
|
67
74
|
[key]: rows,
|
|
68
75
|
[dataset.id]: rows
|
|
69
76
|
};
|
|
70
77
|
const targets = new Set();
|
|
71
|
-
for (const
|
|
72
|
-
var _rows_model1, _field_reference;
|
|
73
|
-
const field = (_rows_model1 = rows.model) == null ? void 0 : _rows_model1.fields[fieldId];
|
|
74
|
-
const targetId = (field == null ? void 0 : field.type) === 'reference' ? (_field_reference = field.reference) == null ? void 0 : _field_reference.datasetId : undefined;
|
|
78
|
+
for (const targetId of Object.values((_ref = (_rows_model = rows.model) == null ? void 0 : _rows_model.references) != null ? _ref : {})){
|
|
75
79
|
if (targetId && !targetId.includes('/') && !rowsByKey[targetId]) {
|
|
76
80
|
targets.add(targetId);
|
|
77
81
|
}
|
|
@@ -94,6 +98,20 @@ import { collection, doc, documentId, getDoc, getDocs, limit, orderBy, query, wh
|
|
|
94
98
|
rowsByKey
|
|
95
99
|
};
|
|
96
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* This site's record template for a dataset (AGL-3475), so a copy previews
|
|
103
|
+
* the `{{item.url}}` the page renders. `null` when there is none, or when the
|
|
104
|
+
* read is refused — a preview without links rather than no preview.
|
|
105
|
+
*/ async function readRecordPageBinding(firestore, hostId, datasetId) {
|
|
106
|
+
try {
|
|
107
|
+
const found = await getDocs(query(collection(firestore, 'hosts', hostId, DATASET_RECORD_PAGES_COLLECTION), where('datasetId', '==', datasetId), limit(1)));
|
|
108
|
+
const [binding] = found.docs;
|
|
109
|
+
return binding ? parseRecordPageBinding(binding.id, binding.data()) : null;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(error);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
97
115
|
/** Both bounded reads, the second only when the first came back short. */ async function readRepeatRows(datasetRef) {
|
|
98
116
|
const records = collection(datasetRef, 'records');
|
|
99
117
|
const page = (snapshot)=>snapshot.docs.map((record)=>({
|
|
@@ -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 (`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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"}
|
|
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 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 './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'\nimport { effectiveDatasetModel, repeatRowsModelOf } from '../model/dataset-models'\nimport { type HostDataset, type HostDatasetRecord, datasetDisplayName } from '../model/datasets'\nimport {\n DATASET_RECORD_PAGES_COLLECTION,\n parseRecordPageBinding,\n withRecordPageUrls,\n} from '../record-pages/record-pages'\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 no copies of the element (AGL-3496), and\n * the canvas badge says so.\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 (\n snapshot: DocumentSnapshot,\n page?: ReturnType<typeof parseRecordPageBinding>,\n ): Promise<RepeatableDataset> => {\n const model = effectiveDatasetModel(snapshot.data() as HostDataset)\n return {\n records: withRecordPageUrls(await readRepeatRows(snapshot.ref), page, model),\n model: repeatRowsModelOf(model),\n }\n }\n const rows = await load(dataset, await readRecordPageBinding(firestore, hostId, dataset.id))\n const rowsByKey: Record<string, RepeatableDataset> = {\n [key]: rows,\n [dataset.id]: rows,\n }\n\n const targets = new Set<string>()\n for (const targetId of Object.values(rows.model?.references ?? {})) {\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/**\n * This site's record template for a dataset (AGL-3475), so a copy previews\n * the `{{item.url}}` the page renders. `null` when there is none, or when the\n * read is refused — a preview without links rather than no preview.\n */\nasync function readRecordPageBinding(\n firestore: Firestore,\n hostId: string,\n datasetId: string,\n) {\n try {\n const found = await getDocs(\n query(\n collection(firestore, 'hosts', hostId, DATASET_RECORD_PAGES_COLLECTION),\n where('datasetId', '==', datasetId),\n limit(1),\n ),\n )\n const [binding] = found.docs\n return binding ? parseRecordPageBinding(binding.id, binding.data()) : null\n } catch (error) {\n console.error(error)\n return null\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":["REPEAT_MAX_RECORDS","scopeTokensForHost","visibleToHost","repeatRecordsFromPages","collection","doc","documentId","getDoc","getDocs","limit","orderBy","query","where","effectiveDatasetModel","repeatRowsModelOf","datasetDisplayName","DATASET_RECORD_PAGES_COLLECTION","parseRecordPageBinding","withRecordPageUrls","readDatasetRepeatRows","request","rows","firestore","scope","hostId","key","trim","status","datasets","usable","snapshot","Boolean","exists","get","dataset","resolvedById","includes","byId","undefined","byName","docs","find","load","page","model","data","records","readRepeatRows","ref","readRecordPageBinding","id","rowsByKey","targets","Set","targetId","Object","values","references","add","Promise","all","map","target","error","console","label","datasetId","found","binding","datasetRef","record","byOrder","length"],"mappings":"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"}
|
|
@@ -33,7 +33,7 @@ import type { RepeatRowsAnswer, RepeatRowsRequest } from '@aglyn/aglyn/plugin-ma
|
|
|
33
33
|
* template once with nothing to say why.
|
|
34
34
|
*
|
|
35
35
|
* Fail-open, per key: a key that cannot be read is left out, and its
|
|
36
|
-
* repeatable renders
|
|
36
|
+
* repeatable renders no copies (AGL-3496), without costing the others.
|
|
37
37
|
*
|
|
38
38
|
* Scoped to what THIS host may see (AGL-1039): a display name resolves inside
|
|
39
39
|
* the host's scope, and an id outside it resolves to nothing. The Admin SDK
|
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
16
|
+
*/ import { REPEAT_MAX_RECORDS, visibleToHost } from "@aglyn/aglyn/server";
|
|
17
17
|
import { orgDataQueryForHost } from "@aglyn/tenant-data-admin";
|
|
18
18
|
import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
|
|
19
|
-
import { repeatRecordsFromPages } from "
|
|
19
|
+
import { repeatRecordsFromPages } from "./repeat-record-pages.js";
|
|
20
20
|
import { FieldPath } from "firebase-admin/firestore";
|
|
21
|
+
import { effectiveDatasetModel, repeatRowsModelOf } from "../model/dataset-models.js";
|
|
22
|
+
import { readSiteRecordPageBindings } from "../record-pages/record-page-read.server.js";
|
|
23
|
+
import { withRecordPageUrls } from "../record-pages/record-pages.js";
|
|
21
24
|
/**
|
|
22
25
|
* The render path's largest read (AGL-1302): up to two pages of records per
|
|
23
26
|
* dataset a page repeats over. Records are mapped to plain values before
|
|
@@ -42,7 +45,7 @@ import { FieldPath } from "firebase-admin/firestore";
|
|
|
42
45
|
* template once with nothing to say why.
|
|
43
46
|
*
|
|
44
47
|
* Fail-open, per key: a key that cannot be read is left out, and its
|
|
45
|
-
* repeatable renders
|
|
48
|
+
* repeatable renders no copies (AGL-3496), without costing the others.
|
|
46
49
|
*
|
|
47
50
|
* Scoped to what THIS host may see (AGL-1039): a display name resolves inside
|
|
48
51
|
* the host's scope, and an id outside it resolves to nothing. The Admin SDK
|
|
@@ -53,10 +56,19 @@ import { FieldPath } from "firebase-admin/firestore";
|
|
|
53
56
|
...new Set(options.keys.map((key)=>key.trim()).filter(Boolean))
|
|
54
57
|
].sort();
|
|
55
58
|
if (!keys.length) return {};
|
|
59
|
+
// The site's record templates (AGL-3475), read through their own cache: a
|
|
60
|
+
// row of a dataset that has one carries its page as `url`.
|
|
61
|
+
const bindings = await readSiteRecordPageBindings(options.hostId).catch((error)=>{
|
|
62
|
+
console.error(error);
|
|
63
|
+
return [];
|
|
64
|
+
});
|
|
56
65
|
try {
|
|
57
66
|
return await withRenderCache({
|
|
67
|
+
// `rows-v3`: rows carry their record page's `url` (AGL-3475); `v2`
|
|
68
|
+
// carried the rows' references rather than the whole model (AGL-3080).
|
|
58
69
|
key: [
|
|
59
70
|
'tenant-datasets',
|
|
71
|
+
'rows-v3',
|
|
60
72
|
options.hostId,
|
|
61
73
|
...keys
|
|
62
74
|
],
|
|
@@ -64,14 +76,14 @@ import { FieldPath } from "firebase-admin/firestore";
|
|
|
64
76
|
tags: [
|
|
65
77
|
tenantDataTag(options.hostId)
|
|
66
78
|
],
|
|
67
|
-
read: ()=>readDatasets(options.hostId, keys)
|
|
79
|
+
read: ()=>readDatasets(options.hostId, keys, bindings)
|
|
68
80
|
});
|
|
69
81
|
} catch (error) {
|
|
70
82
|
console.error(error);
|
|
71
|
-
return readDatasets(options.hostId, keys);
|
|
83
|
+
return readDatasets(options.hostId, keys, bindings);
|
|
72
84
|
}
|
|
73
85
|
}
|
|
74
|
-
async function readDatasets(hostId, keys) {
|
|
86
|
+
async function readDatasets(hostId, keys, bindings = []) {
|
|
75
87
|
const datasets = {};
|
|
76
88
|
try {
|
|
77
89
|
var _ref_parent_parent, _ref_parent;
|
|
@@ -84,9 +96,11 @@ async function readDatasets(hostId, keys) {
|
|
|
84
96
|
const load = (snapshot)=>{
|
|
85
97
|
let dataset = loads.get(snapshot.id);
|
|
86
98
|
if (!dataset) {
|
|
99
|
+
const model = effectiveDatasetModel(snapshot.data());
|
|
100
|
+
const binding = bindings.find((one)=>one.datasetId === snapshot.id);
|
|
87
101
|
dataset = readRepeatRecords(snapshot.ref).then((records)=>({
|
|
88
|
-
records,
|
|
89
|
-
model:
|
|
102
|
+
records: withRecordPageUrls(records, binding, model),
|
|
103
|
+
model: repeatRowsModelOf(model)
|
|
90
104
|
}));
|
|
91
105
|
loads.set(snapshot.id, dataset);
|
|
92
106
|
}
|
|
@@ -122,10 +136,7 @@ async function readDatasets(hostId, keys) {
|
|
|
122
136
|
for (const dataset of resolved){
|
|
123
137
|
var _ref;
|
|
124
138
|
var _dataset_model;
|
|
125
|
-
for (const
|
|
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;
|
|
139
|
+
for (const targetId of Object.values((_ref = dataset == null ? void 0 : (_dataset_model = dataset.model) == null ? void 0 : _dataset_model.references) != null ? _ref : {})){
|
|
129
140
|
if (targetId && !targetId.includes('/') && !datasets[targetId]) {
|
|
130
141
|
targets.add(targetId);
|
|
131
142
|
}
|
|
@@ -1 +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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"}
|
|
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 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 './repeat-record-pages'\nimport { FieldPath } from 'firebase-admin/firestore'\nimport { effectiveDatasetModel, repeatRowsModelOf } from '../model/dataset-models'\nimport type { HostDataset, HostDatasetRecord } from '../model/datasets'\nimport { readSiteRecordPageBindings } from '../record-pages/record-page-read.server'\nimport {\n type DatasetRecordPageBinding,\n withRecordPageUrls,\n} from '../record-pages/record-pages'\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 no copies (AGL-3496), 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 // The site's record templates (AGL-3475), read through their own cache: a\n // row of a dataset that has one carries its page as `url`.\n const bindings = await readSiteRecordPageBindings(options.hostId).catch(\n (error: unknown) => {\n console.error(error)\n return [] as DatasetRecordPageBinding[]\n },\n )\n try {\n return await withRenderCache({\n // `rows-v3`: rows carry their record page's `url` (AGL-3475); `v2`\n // carried the rows' references rather than the whole model (AGL-3080).\n key: ['tenant-datasets', 'rows-v3', options.hostId, ...keys],\n revalidate: DATASETS_TTL_SECONDS,\n tags: [tenantDataTag(options.hostId)],\n read: () => readDatasets(options.hostId, keys, bindings),\n })\n } catch (error) {\n console.error(error)\n return readDatasets(options.hostId, keys, bindings)\n }\n}\n\nasync function readDatasets(\n hostId: string,\n keys: readonly string[],\n bindings: readonly DatasetRecordPageBinding[] = [],\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 const model = effectiveDatasetModel(snapshot.data() as HostDataset)\n const binding = bindings.find((one) => one.datasetId === snapshot.id)\n dataset = readRepeatRecords(snapshot.ref).then((records) => ({\n records: withRecordPageUrls(records, binding, model),\n model: repeatRowsModelOf(model),\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 targetId of Object.values(dataset?.model?.references ?? {})) {\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":["REPEAT_MAX_RECORDS","visibleToHost","orgDataQueryForHost","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","repeatRecordsFromPages","FieldPath","effectiveDatasetModel","repeatRowsModelOf","readSiteRecordPageBindings","withRecordPageUrls","DATASETS_TTL_SECONDS","readPublishedDatasetRows","options","keys","Set","map","key","trim","filter","Boolean","sort","length","bindings","hostId","catch","error","console","revalidate","tags","read","readDatasets","datasets","ref","query","orgScoped","parent","id","usable","snapshot","exists","get","loads","Map","load","dataset","model","data","binding","find","one","datasetId","readRepeatRecords","then","records","set","resolveKey","includes","byId","doc","undefined","byName","where","limit","docs","resolved","Promise","all","targets","targetId","Object","values","references","add","target","datasetRef","recordsRef","collection","page","byOrder","orderBy","documentId"],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
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 HostDatasetRecord } from '../model/datasets';
|
|
18
|
+
/** One record document as either Firestore SDK hands it back. */
|
|
19
|
+
export interface RepeatRecordSnapshot {
|
|
20
|
+
id: string;
|
|
21
|
+
data: HostDatasetRecord;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The records a repeat renders, in the order it renders them, from the two
|
|
25
|
+
* bounded reads that find them (AGL-1302).
|
|
26
|
+
*
|
|
27
|
+
* The rows a repeat shows are the ones `sortDatasetRecords` puts first: every
|
|
28
|
+
* record with an editor `order`, ascending, then the records without one —
|
|
29
|
+
* forms and Actions append those — by document id. A `limit()` with no
|
|
30
|
+
* `orderBy` cannot find them: Firestore answers it in document-id order, so on
|
|
31
|
+
* a dataset past the bound it reads an arbitrary sample, and sorting that
|
|
32
|
+
* sample afterwards only makes it look ordered.
|
|
33
|
+
*
|
|
34
|
+
* `byOrder` is the page ordered by `order`, bounded by
|
|
35
|
+
* {@link REPEAT_MAX_RECORDS}, which matches only documents that carry the
|
|
36
|
+
* field. When it comes back short every ordered record is already in hand, so
|
|
37
|
+
* `byId` — the same bound over the document id — supplies exactly the first
|
|
38
|
+
* unordered ones; a caller whose `byOrder` came back full does not need to
|
|
39
|
+
* read it, and passes an empty list.
|
|
40
|
+
*
|
|
41
|
+
* The published page's composition and the besigner's canvas preview both
|
|
42
|
+
* read through this, with the Admin SDK and the client SDK respectively, so
|
|
43
|
+
* the canvas cannot preview a row the page would not render.
|
|
44
|
+
*
|
|
45
|
+
* Each row is the record's value map, with `$id` inside it so incoming
|
|
46
|
+
* reference hops (AGL-180) can resolve rows.
|
|
47
|
+
*/
|
|
48
|
+
export declare function repeatRecordsFromPages(byOrder: readonly RepeatRecordSnapshot[], byId: readonly RepeatRecordSnapshot[]): Array<Record<string, unknown>>;
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
*/ // By path, not `@aglyn/aglyn/server`: this module is also read in the browser
|
|
18
|
+
// (the besigner's repeat preview, AGL-3111), and the server entry carries
|
|
19
|
+
// `node:` modules no browser bundle can hold.
|
|
20
|
+
import { sortDatasetRecords } from "../model/datasets.js";
|
|
21
|
+
import { REPEAT_MAX_RECORDS } from "@aglyn/aglyn/app-utils/expand-repeatables";
|
|
22
|
+
/**
|
|
23
|
+
* The records a repeat renders, in the order it renders them, from the two
|
|
24
|
+
* bounded reads that find them (AGL-1302).
|
|
25
|
+
*
|
|
26
|
+
* The rows a repeat shows are the ones `sortDatasetRecords` puts first: every
|
|
27
|
+
* record with an editor `order`, ascending, then the records without one —
|
|
28
|
+
* forms and Actions append those — by document id. A `limit()` with no
|
|
29
|
+
* `orderBy` cannot find them: Firestore answers it in document-id order, so on
|
|
30
|
+
* a dataset past the bound it reads an arbitrary sample, and sorting that
|
|
31
|
+
* sample afterwards only makes it look ordered.
|
|
32
|
+
*
|
|
33
|
+
* `byOrder` is the page ordered by `order`, bounded by
|
|
34
|
+
* {@link REPEAT_MAX_RECORDS}, which matches only documents that carry the
|
|
35
|
+
* field. When it comes back short every ordered record is already in hand, so
|
|
36
|
+
* `byId` — the same bound over the document id — supplies exactly the first
|
|
37
|
+
* unordered ones; a caller whose `byOrder` came back full does not need to
|
|
38
|
+
* read it, and passes an empty list.
|
|
39
|
+
*
|
|
40
|
+
* The published page's composition and the besigner's canvas preview both
|
|
41
|
+
* read through this, with the Admin SDK and the client SDK respectively, so
|
|
42
|
+
* the canvas cannot preview a row the page would not render.
|
|
43
|
+
*
|
|
44
|
+
* Each row is the record's value map, with `$id` inside it so incoming
|
|
45
|
+
* reference hops (AGL-180) can resolve rows.
|
|
46
|
+
*/ export function repeatRecordsFromPages(byOrder, byId) {
|
|
47
|
+
const snapshots = [
|
|
48
|
+
...byOrder
|
|
49
|
+
];
|
|
50
|
+
if (byOrder.length < REPEAT_MAX_RECORDS) {
|
|
51
|
+
const held = new Set(byOrder.map((snapshot)=>snapshot.id));
|
|
52
|
+
snapshots.push(...byId.filter((snapshot)=>!held.has(snapshot.id)));
|
|
53
|
+
}
|
|
54
|
+
return sortDatasetRecords(snapshots.map((snapshot)=>_extends({
|
|
55
|
+
$id: snapshot.id
|
|
56
|
+
}, snapshot.data))).slice(0, REPEAT_MAX_RECORDS).map((record)=>{
|
|
57
|
+
var _record_values;
|
|
58
|
+
return _extends({}, (_record_values = record.values) != null ? _record_values : {}, {
|
|
59
|
+
$id: record.$id
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=repeat-record-pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/repeat/repeat-record-pages.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// By path, not `@aglyn/aglyn/server`: this module is also read in the browser\n// (the besigner's repeat preview, AGL-3111), and the server entry carries\n// `node:` modules no browser bundle can hold.\nimport {\n type HostDatasetRecord,\n sortDatasetRecords,\n} from '../model/datasets'\nimport { REPEAT_MAX_RECORDS } from '@aglyn/aglyn/app-utils/expand-repeatables'\n\n/** One record document as either Firestore SDK hands it back. */\nexport interface RepeatRecordSnapshot {\n id: string\n data: HostDatasetRecord\n}\n\n/**\n * The records a repeat renders, in the order it renders them, from the two\n * bounded reads that find them (AGL-1302).\n *\n * The rows a repeat shows are the ones `sortDatasetRecords` puts first: every\n * record with an editor `order`, ascending, then the records without one —\n * forms and Actions append those — by document id. A `limit()` with no\n * `orderBy` cannot find them: Firestore answers it in document-id order, so on\n * a dataset past the bound it reads an arbitrary sample, and sorting that\n * sample afterwards only makes it look ordered.\n *\n * `byOrder` is the page ordered by `order`, bounded by\n * {@link REPEAT_MAX_RECORDS}, which matches only documents that carry the\n * field. When it comes back short every ordered record is already in hand, so\n * `byId` — the same bound over the document id — supplies exactly the first\n * unordered ones; a caller whose `byOrder` came back full does not need to\n * read it, and passes an empty list.\n *\n * The published page's composition and the besigner's canvas preview both\n * read through this, with the Admin SDK and the client SDK respectively, so\n * the canvas cannot preview a row the page would not render.\n *\n * Each row is the record's value map, with `$id` inside it so incoming\n * reference hops (AGL-180) can resolve rows.\n */\nexport function repeatRecordsFromPages(\n byOrder: readonly RepeatRecordSnapshot[],\n byId: readonly RepeatRecordSnapshot[],\n): Array<Record<string, unknown>> {\n const snapshots = [...byOrder]\n if (byOrder.length < REPEAT_MAX_RECORDS) {\n const held = new Set(byOrder.map((snapshot) => snapshot.id))\n snapshots.push(...byId.filter((snapshot) => !held.has(snapshot.id)))\n }\n return sortDatasetRecords(\n snapshots.map((snapshot) => ({ $id: snapshot.id, ...snapshot.data })),\n )\n .slice(0, REPEAT_MAX_RECORDS)\n .map((record) => ({ ...(record.values ?? {}), $id: record.$id }))\n}\n"],"names":["sortDatasetRecords","REPEAT_MAX_RECORDS","repeatRecordsFromPages","byOrder","byId","snapshots","length","held","Set","map","snapshot","id","push","filter","has","$id","data","slice","record","values"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,0EAA0E;AAC1E,8CAA8C;AAC9C,SAEEA,kBAAkB,QACb,uBAAmB;AAC1B,SAASC,kBAAkB,QAAQ,4CAA2C;AAQ9E;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GACD,OAAO,SAASC,uBACdC,OAAwC,EACxCC,IAAqC;IAErC,MAAMC,YAAY;WAAIF;KAAQ;IAC9B,IAAIA,QAAQG,MAAM,GAAGL,oBAAoB;QACvC,MAAMM,OAAO,IAAIC,IAAIL,QAAQM,GAAG,CAAC,CAACC,WAAaA,SAASC,EAAE;QAC1DN,UAAUO,IAAI,IAAIR,KAAKS,MAAM,CAAC,CAACH,WAAa,CAACH,KAAKO,GAAG,CAACJ,SAASC,EAAE;IACnE;IACA,OAAOX,mBACLK,UAAUI,GAAG,CAAC,CAACC,WAAc;YAAEK,KAAKL,SAASC,EAAE;WAAKD,SAASM,IAAI,IAEhEC,KAAK,CAAC,GAAGhB,oBACTQ,GAAG,CAAC,CAACS;YAAkBA;eAAN,cAAMA,iBAAAA,OAAOC,MAAM,YAAbD,iBAAiB,CAAC;YAAIH,KAAKG,OAAOH,GAAG;;;AACjE"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type DatasetAnnounceResult } from '
|
|
17
|
+
import { type DatasetAnnounceResult } from './dataset-live-pages';
|
|
18
18
|
import type { Firestore } from 'firebase-admin/firestore';
|
|
19
19
|
/**
|
|
20
20
|
* Refresh every live page showing this dataset's records, from one of this
|
|
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
|
|
18
|
-
import { announceDatasetRecordChange } from "
|
|
18
|
+
import { announceDatasetRecordChange } from "./dataset-live-pages.js";
|
|
19
19
|
/**
|
|
20
20
|
* Drops one site's pages through the platform's site cache, narrowed to the
|
|
21
21
|
* addresses that repeat over the dataset — the app knows how to drop a page on
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/announce-dataset-records.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 { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/server/announce-dataset-records.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 { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport type { LivePageDropper } from '@aglyn/tenant-data-admin/server/live-page-drops'\nimport { announceDatasetRecordChange, type DatasetAnnounceResult } from './dataset-live-pages'\nimport type { Firestore } from 'firebase-admin/firestore'\n\n/**\n * Drops one site's pages through the platform's site cache, narrowed to the\n * addresses that repeat over the dataset — the app knows how to drop a page on\n * every address a site answers at, and this plugin knows which pages.\n */\nconst dropThroughSiteCache: LivePageDropper = async (target) => {\n const result = await dropPluginSiteCache({\n hostIds: [target.hostId],\n paths: { [target.hostId]: target.paths },\n reason: 'dataset records changed',\n })\n return result.complete && result.dropped > 0\n}\n\n/**\n * Refresh every live page showing this dataset's records, from one of this\n * plugin's console routes (AGL-3113).\n *\n * `announceDatasetRecordChange` works out WHICH pages a record write makes\n * stale; the drop goes through the platform's site cache. Callers `await` it:\n * the pages are supposed to be fresh by the time the editor's save returns.\n * The result is returned rather than swallowed so a route can surface the\n * shortfall warning — a refusal is the one case where the write landed and\n * the live pages stay stale for the rest of their window.\n *\n * BEST EFFORT, ALWAYS. The record has already been written by the time this\n * runs, so it never throws and never rejects.\n */\nexport async function announceDatasetRecords(options: {\n firestore: Firestore\n orgId: string\n datasetId: string\n}): Promise<DatasetAnnounceResult> {\n return announceDatasetRecordChange({ ...options, drop: dropThroughSiteCache })\n}\n"],"names":["dropPluginSiteCache","announceDatasetRecordChange","dropThroughSiteCache","target","result","hostIds","hostId","paths","reason","complete","dropped","announceDatasetRecords","options","drop"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,gDAA+C;AAEnF,SAASC,2BAA2B,QAAoC,0BAAsB;AAG9F;;;;CAIC,GACD,MAAMC,uBAAwC,OAAOC;IACnD,MAAMC,SAAS,MAAMJ,oBAAoB;QACvCK,SAAS;YAACF,OAAOG,MAAM;SAAC;QACxBC,OAAO;YAAE,CAACJ,OAAOG,MAAM,CAAC,EAAEH,OAAOI,KAAK;QAAC;QACvCC,QAAQ;IACV;IACA,OAAOJ,OAAOK,QAAQ,IAAIL,OAAOM,OAAO,GAAG;AAC7C;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAeC,uBAAuBC,OAI5C;IACC,OAAOX,4BAA4B,aAAKW;QAASC,MAAMX;;AACzD"}
|
|
@@ -14,12 +14,14 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { checkDatasetQuota, checkEntitlement, checkQuota,
|
|
17
|
+
*/ import { checkDatasetQuota, checkEntitlement, checkQuota, createResourceUid, defaultScopeForNewResource, newResourceScopeFields } from "@aglyn/aglyn/server";
|
|
18
18
|
import { ApiErrors, apiJson, dataStorageRefusal, listResponse } from "@aglyn/tenant-data-admin";
|
|
19
19
|
import { claimWrite, paginate, readJsonBody, requireScope, serialize } from "@aglyn/tenant-data-admin/server/api-v1-kit";
|
|
20
20
|
import { FieldValue, Timestamp } from "firebase-admin/firestore";
|
|
21
21
|
import { announceDatasetRecords as announceDatasetChange } from "../announce-dataset-records.js";
|
|
22
22
|
import { loadCustomFieldTypes } from "../custom-field-types.js";
|
|
23
|
+
import { coerceDocumentValues, datasetIntegrityFields, datasetIntegrityUpdate, effectiveDatasetModel, validateDocument } from "../../model/dataset-models.js";
|
|
24
|
+
import { fillRecordAddresses } from "../../record-pages/record-pages.js";
|
|
23
25
|
/**
|
|
24
26
|
* The organization's datasets on the customer REST API, `/v1/datasets/…`
|
|
25
27
|
* (AGL-2126, AGL-3080): the datasets themselves and the records inside them.
|
|
@@ -414,7 +416,7 @@ async function createRecord(request, ctx, datasetSnap, recordsRef) {
|
|
|
414
416
|
// A plugin's field validator only runs once its plugin has registered it.
|
|
415
417
|
await loadCustomFieldTypes(model, ctx.loadPluginSurfaces);
|
|
416
418
|
const body = await readJsonBody(request);
|
|
417
|
-
const coerced = coerceDocumentValues(model, (_body_values = body.values) != null ? _body_values : {});
|
|
419
|
+
const coerced = fillRecordAddresses(model, coerceDocumentValues(model, (_body_values = body.values) != null ? _body_values : {}));
|
|
418
420
|
const errors = validateDocument(model, coerced);
|
|
419
421
|
if (Object.keys(errors).length) {
|
|
420
422
|
return ApiErrors.badRequest({
|
|
@@ -538,7 +540,7 @@ async function updateRecord(request, ctx, datasetSnap, recordRef) {
|
|
|
538
540
|
await loadCustomFieldTypes(model, ctx.loadPluginSurfaces);
|
|
539
541
|
const body = await readJsonBody(request);
|
|
540
542
|
// PATCH merges the supplied fields over the stored values.
|
|
541
|
-
const merged = _extends({}, (_snap_get = snap.get('values')) != null ? _snap_get : {}, coerceDocumentValues(model, (_body_values = body.values) != null ? _body_values : {}));
|
|
543
|
+
const merged = fillRecordAddresses(model, _extends({}, (_snap_get = snap.get('values')) != null ? _snap_get : {}, coerceDocumentValues(model, (_body_values = body.values) != null ? _body_values : {})));
|
|
542
544
|
const errors = validateDocument(model, merged);
|
|
543
545
|
if (Object.keys(errors).length) {
|
|
544
546
|
return ApiErrors.badRequest({
|