@aglyn/plugins-data 1.0.0-beta.221 → 1.0.0-beta.222
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,218 @@
|
|
|
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 { RepeatRowsModel } from '@aglyn/aglyn/app-utils/expand-repeatables';
|
|
18
|
+
import type { CustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields';
|
|
19
|
+
import type { DatasetFieldDefinition, DatasetModel } from '../model/dataset-models';
|
|
20
|
+
/** This plugin's host subcollection the bindings live in. Persisted — never rename. */
|
|
21
|
+
export declare const DATASET_RECORD_PAGES_COLLECTION = "recordPages";
|
|
22
|
+
/**
|
|
23
|
+
* The custom field type a record's page address rides (`text` storage).
|
|
24
|
+
* Persisted on field definitions — never rename.
|
|
25
|
+
*/
|
|
26
|
+
export declare const RECORD_PAGE_ADDRESS_FIELD_TYPE = "pageAddress";
|
|
27
|
+
/**
|
|
28
|
+
* The longest page address, in characters. It is the length a plain text
|
|
29
|
+
* value's `filterValues` entry is clipped to (`DATASET_FILTER_VALUE_MAX`), so
|
|
30
|
+
* every address a record can hold is one the indexed lookup can find whole.
|
|
31
|
+
*/
|
|
32
|
+
export declare const RECORD_PAGE_ADDRESS_MAX = 64;
|
|
33
|
+
/** The most path segments a base may have: `services/residential/roofing`. */
|
|
34
|
+
export declare const RECORD_PAGE_BASE_MAX_SEGMENTS = 4;
|
|
35
|
+
/** The longest single base segment. */
|
|
36
|
+
export declare const RECORD_PAGE_BASE_SEGMENT_MAX = 60;
|
|
37
|
+
/** How many record templates one site may hold. */
|
|
38
|
+
export declare const RECORD_PAGES_MAX_PER_SITE = 50;
|
|
39
|
+
/**
|
|
40
|
+
* A site's record template, as `hosts/{hostId}/recordPages/{screenId}` holds
|
|
41
|
+
* it. The document id is the template screen's id, so a page is the template
|
|
42
|
+
* of at most one dataset.
|
|
43
|
+
*/
|
|
44
|
+
export interface DatasetRecordPageBinding {
|
|
45
|
+
/** The template screen — also the document id. */
|
|
46
|
+
screenId: string;
|
|
47
|
+
/** The organization dataset whose records the pages render. */
|
|
48
|
+
datasetId: string;
|
|
49
|
+
/**
|
|
50
|
+
* The path the pages are served under, without slashes at either end: one
|
|
51
|
+
* or more segments (`services`, `services/residential`).
|
|
52
|
+
*/
|
|
53
|
+
base: string;
|
|
54
|
+
/** The record's page-address field. */
|
|
55
|
+
slugField: string;
|
|
56
|
+
/**
|
|
57
|
+
* The field that NAMES a record page — the tab title's first half, the
|
|
58
|
+
* breadcrumb, the sitemap's view of it. Absent, the dataset's first text
|
|
59
|
+
* field does.
|
|
60
|
+
*/
|
|
61
|
+
titleField?: string;
|
|
62
|
+
/** A field whose value is the search title, used verbatim. */
|
|
63
|
+
seoTitleField?: string;
|
|
64
|
+
/** A field whose value is the search description. */
|
|
65
|
+
seoDescriptionField?: string;
|
|
66
|
+
/** A field holding the sharing image: a media reference or a URL. */
|
|
67
|
+
seoImageField?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A record's page address from any text: the platform's URL segment
|
|
71
|
+
* (`urlSlugSegment`, the rule category and author pages use) of the text with
|
|
72
|
+
* its accents folded first, so `Café` is `cafe` rather than `caf`, and at most
|
|
73
|
+
* {@link RECORD_PAGE_ADDRESS_MAX} characters. Idempotent, so an address that
|
|
74
|
+
* is already one comes back unchanged — `Kitchen Remodeling` →
|
|
75
|
+
* `kitchen-remodeling` → `kitchen-remodeling`. `''` for text with nothing
|
|
76
|
+
* addressable in it.
|
|
77
|
+
*/
|
|
78
|
+
export declare function normalizeRecordAddress(value: unknown): string;
|
|
79
|
+
/**
|
|
80
|
+
* The "Page address" field type, as the custom-field registry holds it: text
|
|
81
|
+
* storage, offered in the schema dialog, and checked on every write path
|
|
82
|
+
* after the address has been normalized (`fillRecordAddresses`), so what it
|
|
83
|
+
* refuses is an address with nothing addressable left in it, never one typed
|
|
84
|
+
* with a capital letter.
|
|
85
|
+
*/
|
|
86
|
+
export declare const RECORD_PAGE_ADDRESS_FIELD: CustomFieldType;
|
|
87
|
+
/** Whether a value is already a page address, exactly as stored. */
|
|
88
|
+
export declare function isRecordAddress(value: unknown): value is string;
|
|
89
|
+
/** The page address a record holds in its address field, or `''`. */
|
|
90
|
+
export declare function recordAddressOf(values: Record<string, unknown> | null | undefined, slugField: string): string;
|
|
91
|
+
/**
|
|
92
|
+
* A base as stored: `/Services/Residential/` → `services/residential`. `null`
|
|
93
|
+
* when it is empty, has more than {@link RECORD_PAGE_BASE_MAX_SEGMENTS}
|
|
94
|
+
* segments, or a segment that is not URL-safe once lowercased — a base is
|
|
95
|
+
* typed by a person and refused rather than rewritten, because a rewritten
|
|
96
|
+
* base is a URL they did not choose.
|
|
97
|
+
*/
|
|
98
|
+
export declare function normalizeRecordPageBase(input: unknown): string | null;
|
|
99
|
+
/** What a base must not collide with, on the site it is chosen for. */
|
|
100
|
+
export interface RecordPageBaseContext {
|
|
101
|
+
/** The site's content collections' slugs, which own `/{slug}/…`. */
|
|
102
|
+
collectionSlugs?: readonly string[];
|
|
103
|
+
/**
|
|
104
|
+
* The first segments other plugins serve pages under on every site —
|
|
105
|
+
* `products` for a store — read off their declared sitemap sections.
|
|
106
|
+
*/
|
|
107
|
+
pluginRouteSegments?: readonly string[];
|
|
108
|
+
/** The bases the site's OTHER record templates already use. */
|
|
109
|
+
otherBases?: readonly string[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Why a base cannot be used on this site, as one sentence an author reads, or
|
|
113
|
+
* `null` when it can.
|
|
114
|
+
*
|
|
115
|
+
* Record pages are answered after every published page and before content
|
|
116
|
+
* collections, so a base whose first segment is a collection's would take
|
|
117
|
+
* that collection's entry addresses; the platform's reserved addresses and
|
|
118
|
+
* another plugin's routes are refused for the same reason. Two templates may
|
|
119
|
+
* nest — `services` and `services/residential` serve different depths — but
|
|
120
|
+
* never share a base.
|
|
121
|
+
*/
|
|
122
|
+
export declare function recordPageBaseRefusal(input: unknown, context?: RecordPageBaseContext): string | null;
|
|
123
|
+
/** A record page's site path: `/services/kitchen-remodel`. */
|
|
124
|
+
export declare function recordPagePath(base: string, address: string): string;
|
|
125
|
+
/** The route a record template answers, as the page review names it. */
|
|
126
|
+
export declare function recordPageRoute(base: string): string;
|
|
127
|
+
/**
|
|
128
|
+
* The binding a request path is a record page of, with the address it asks
|
|
129
|
+
* for — or `null`. A path matches a binding when it is the base plus exactly
|
|
130
|
+
* one more segment, so nested bases never compete: `/services/residential`
|
|
131
|
+
* asks `services` for `residential`, and `/services/residential/roofing` asks
|
|
132
|
+
* `services/residential` for `roofing`.
|
|
133
|
+
*
|
|
134
|
+
* The address segment is normalized the way a stored one is, so a request
|
|
135
|
+
* spelled `/services/Kitchen-Remodel` finds the record and the head names the
|
|
136
|
+
* one address that is canonical.
|
|
137
|
+
*/
|
|
138
|
+
export declare function matchRecordPagePath<B extends Pick<DatasetRecordPageBinding, 'base'>>(bindings: readonly B[], path: string): {
|
|
139
|
+
binding: B;
|
|
140
|
+
address: string;
|
|
141
|
+
} | null;
|
|
142
|
+
/** Is this field a page address? */
|
|
143
|
+
export declare function isRecordAddressField(field: DatasetFieldDefinition | null | undefined): boolean;
|
|
144
|
+
/** The model's page-address fields, in display order. */
|
|
145
|
+
export declare function recordAddressFieldIds(model: DatasetModel): string[];
|
|
146
|
+
/**
|
|
147
|
+
* Fills each EMPTY page-address field from the field it names as its source
|
|
148
|
+
* (`slugFrom`), and normalizes every address the values already hold.
|
|
149
|
+
*
|
|
150
|
+
* Run on a record's WHOLE values before every write — a create, an import, a
|
|
151
|
+
* form or automation append, and the merged values of an update — which is
|
|
152
|
+
* what makes "the address fills in once and never moves" true: an update that
|
|
153
|
+
* renames the record arrives with the address it already has, so there is
|
|
154
|
+
* nothing empty to fill. Returns the input itself when nothing changes.
|
|
155
|
+
*/
|
|
156
|
+
export declare function fillRecordAddresses<V extends Record<string, unknown>>(model: DatasetModel, values: V): V;
|
|
157
|
+
/**
|
|
158
|
+
* Unique addresses for a set of records being given one for the first time —
|
|
159
|
+
* `kitchen-remodel`, `kitchen-remodel-2` — in the order given, skipping the
|
|
160
|
+
* addresses already `taken`. A record whose source has nothing addressable in
|
|
161
|
+
* it gets none.
|
|
162
|
+
*/
|
|
163
|
+
export declare function uniqueRecordAddresses(sources: ReadonlyArray<{
|
|
164
|
+
id: string;
|
|
165
|
+
source: unknown;
|
|
166
|
+
}>, taken?: Iterable<string>): Map<string, string>;
|
|
167
|
+
/**
|
|
168
|
+
* The field that names a record page: the binding's `titleField`, else the
|
|
169
|
+
* model's first text field that is not a page address.
|
|
170
|
+
*/
|
|
171
|
+
export declare function recordPageNameField(binding: Pick<DatasetRecordPageBinding, 'titleField' | 'slugField'>, model: DatasetModel): string | undefined;
|
|
172
|
+
/** What a record page says about itself in the head. */
|
|
173
|
+
export interface RecordPageHead {
|
|
174
|
+
/** The page's name: the site title is joined after it. */
|
|
175
|
+
name?: string;
|
|
176
|
+
/** An authored search title, used verbatim. */
|
|
177
|
+
title?: string;
|
|
178
|
+
description?: string;
|
|
179
|
+
/** A media reference or URL, resolved by the head like any page image. */
|
|
180
|
+
image?: string;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The head of one record's page, from the fields the binding picked. Each
|
|
184
|
+
* part is absent when its field is unset or empty, and the head then falls
|
|
185
|
+
* back to the template page's own value, then the site's, exactly as it does
|
|
186
|
+
* for every page.
|
|
187
|
+
*/
|
|
188
|
+
export declare function recordPageHead(binding: DatasetRecordPageBinding, model: DatasetModel, values: Record<string, unknown>): RecordPageHead;
|
|
189
|
+
/**
|
|
190
|
+
* A binding document read back, or `null` when it does not describe one: the
|
|
191
|
+
* fields every reader needs, trimmed. Optional fields that are blank are
|
|
192
|
+
* dropped rather than kept as `''`.
|
|
193
|
+
*/
|
|
194
|
+
export declare function parseRecordPageBinding(screenId: string, data: Record<string, unknown> | null | undefined): DatasetRecordPageBinding | null;
|
|
195
|
+
/**
|
|
196
|
+
* The records a row's reference fields point at, by the dataset each lives in:
|
|
197
|
+
* what a one-hop `{{item.author.name}}` on a record page has to read. A
|
|
198
|
+
* multiple reference contributes every id it holds.
|
|
199
|
+
*/
|
|
200
|
+
export declare function referencedRecordIds(model: RepeatRowsModel, record: Record<string, unknown>): Map<string, Set<string>>;
|
|
201
|
+
/**
|
|
202
|
+
* The row key `{{item.url}}` reads: a record's page on this site.
|
|
203
|
+
* Persisted in nothing — it is computed on every read — but bound by authors,
|
|
204
|
+
* so never rename.
|
|
205
|
+
*/
|
|
206
|
+
export declare const RECORD_PAGE_URL_KEY = "url";
|
|
207
|
+
/**
|
|
208
|
+
* A dataset's repeated rows, each carrying its page as `url` when this site
|
|
209
|
+
* has a record template for the dataset (AGL-3475) — so a listing card links
|
|
210
|
+
* to the record's page with `{{item.url}}`, and the canvas previews the same
|
|
211
|
+
* link the page renders.
|
|
212
|
+
*
|
|
213
|
+
* A model that declares a field of its own called `url` keeps it: the token
|
|
214
|
+
* always means the dataset's field first. A row with no address has no page,
|
|
215
|
+
* and so no `url`, and its token stays as written. Returns the rows
|
|
216
|
+
* themselves when there is nothing to add.
|
|
217
|
+
*/
|
|
218
|
+
export declare function withRecordPageUrls<R extends Record<string, unknown>>(rows: R[], binding: Pick<DatasetRecordPageBinding, 'base' | 'slugField'> | null | undefined, model: DatasetModel): R[];
|
|
@@ -0,0 +1,351 @@
|
|
|
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
|
+
*/ /**
|
|
18
|
+
* Record pages (AGL-3475): one page per dataset record, rendered from one
|
|
19
|
+
* template page.
|
|
20
|
+
*
|
|
21
|
+
* A site marks a page as the RECORD TEMPLATE of a dataset and names a base
|
|
22
|
+
* path, and every record of that dataset with a page address is served at
|
|
23
|
+
* `/{base}/{address}` — a services dataset becomes `/services/kitchen-remodel`,
|
|
24
|
+
* `/services/roofing`, and so on, all drawn by the one design. Inside the
|
|
25
|
+
* template `{{item.field}}` means exactly what it means inside a repeat
|
|
26
|
+
* (`substituteRecordTokens`, one reference hop), so an author who has built a
|
|
27
|
+
* repeated card already knows how to build the page it links to.
|
|
28
|
+
*
|
|
29
|
+
* ## What lives where
|
|
30
|
+
*
|
|
31
|
+
* - The BINDING — which page, which dataset, which base, which fields — is a
|
|
32
|
+
* document of this plugin's own, `hosts/{hostId}/recordPages/{screenId}`.
|
|
33
|
+
* It is per site because the base and the page are; the dataset is the
|
|
34
|
+
* organization's, so one dataset can have a record template on every site
|
|
35
|
+
* it is shared with, each at its own base.
|
|
36
|
+
* - The ADDRESS is a field on the record: a text field of the "Page address"
|
|
37
|
+
* custom type ({@link RECORD_PAGE_ADDRESS_FIELD_TYPE}). It is stored rather
|
|
38
|
+
* than derived at read time for two reasons. A stored address is what a
|
|
39
|
+
* query can find — `filterValues.<field>` is already indexed on every record
|
|
40
|
+
* (AGL-3321) — and it is what keeps a live URL where it is when somebody
|
|
41
|
+
* renames the record. An address field may name a SOURCE field
|
|
42
|
+
* (`slugFrom`): an empty address fills in from it once, on whichever write
|
|
43
|
+
* first finds it empty, and never moves afterwards.
|
|
44
|
+
* - The TEMPLATE is an ordinary screen stamped `kind: 'template'`, the stamp a
|
|
45
|
+
* blog's entry template carries: it renders at no address of its own, it is
|
|
46
|
+
* not one of the plan's pages, and its raw `{{item.*}}` never ships.
|
|
47
|
+
*
|
|
48
|
+
* Pure and SDK-free: the console's settings, the canvas preview and the
|
|
49
|
+
* tenant's resolver all read it.
|
|
50
|
+
*/ import { AUTHOR_ROUTE_SEGMENT } from "@aglyn/aglyn/app-utils/content-authors";
|
|
51
|
+
import { reservedScreenRouteSegment } from "@aglyn/aglyn/app-utils/screen-route";
|
|
52
|
+
import { urlSlugSegment } from "@aglyn/aglyn/app-utils/url-slug";
|
|
53
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
54
|
+
/** This plugin's host subcollection the bindings live in. Persisted — never rename. */ export const DATASET_RECORD_PAGES_COLLECTION = 'recordPages';
|
|
55
|
+
/**
|
|
56
|
+
* The custom field type a record's page address rides (`text` storage).
|
|
57
|
+
* Persisted on field definitions — never rename.
|
|
58
|
+
*/ export const RECORD_PAGE_ADDRESS_FIELD_TYPE = 'pageAddress';
|
|
59
|
+
/**
|
|
60
|
+
* The longest page address, in characters. It is the length a plain text
|
|
61
|
+
* value's `filterValues` entry is clipped to (`DATASET_FILTER_VALUE_MAX`), so
|
|
62
|
+
* every address a record can hold is one the indexed lookup can find whole.
|
|
63
|
+
*/ export const RECORD_PAGE_ADDRESS_MAX = 64;
|
|
64
|
+
/** The most path segments a base may have: `services/residential/roofing`. */ export const RECORD_PAGE_BASE_MAX_SEGMENTS = 4;
|
|
65
|
+
/** The longest single base segment. */ export const RECORD_PAGE_BASE_SEGMENT_MAX = 60;
|
|
66
|
+
/** How many record templates one site may hold. */ export const RECORD_PAGES_MAX_PER_SITE = 50;
|
|
67
|
+
/** One URL-safe segment: lowercase letters and digits joined by single hyphens. */ const SEGMENT = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
68
|
+
/**
|
|
69
|
+
* A record's page address from any text: the platform's URL segment
|
|
70
|
+
* (`urlSlugSegment`, the rule category and author pages use) of the text with
|
|
71
|
+
* its accents folded first, so `Café` is `cafe` rather than `caf`, and at most
|
|
72
|
+
* {@link RECORD_PAGE_ADDRESS_MAX} characters. Idempotent, so an address that
|
|
73
|
+
* is already one comes back unchanged — `Kitchen Remodeling` →
|
|
74
|
+
* `kitchen-remodeling` → `kitchen-remodeling`. `''` for text with nothing
|
|
75
|
+
* addressable in it.
|
|
76
|
+
*/ export function normalizeRecordAddress(value) {
|
|
77
|
+
if (typeof value !== 'string' && typeof value !== 'number') return '';
|
|
78
|
+
return urlSlugSegment(String(value).normalize('NFKD').replace(/[̀-ͯ]/g, '')).slice(0, RECORD_PAGE_ADDRESS_MAX).replace(/-+$/g, '');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The "Page address" field type, as the custom-field registry holds it: text
|
|
82
|
+
* storage, offered in the schema dialog, and checked on every write path
|
|
83
|
+
* after the address has been normalized (`fillRecordAddresses`), so what it
|
|
84
|
+
* refuses is an address with nothing addressable left in it, never one typed
|
|
85
|
+
* with a capital letter.
|
|
86
|
+
*/ export const RECORD_PAGE_ADDRESS_FIELD = {
|
|
87
|
+
name: RECORD_PAGE_ADDRESS_FIELD_TYPE,
|
|
88
|
+
pluginId: BUNDLE_ID,
|
|
89
|
+
label: 'Page address',
|
|
90
|
+
baseType: 'text',
|
|
91
|
+
description: 'The record’s own page under a record template: lowercase words joined by hyphens',
|
|
92
|
+
validate: (value)=>isRecordAddress(value) ? null : `Use lowercase letters, numbers and hyphens, up to ${RECORD_PAGE_ADDRESS_MAX} characters`
|
|
93
|
+
};
|
|
94
|
+
/** Whether a value is already a page address, exactly as stored. */ export function isRecordAddress(value) {
|
|
95
|
+
return typeof value === 'string' && value.length <= RECORD_PAGE_ADDRESS_MAX && SEGMENT.test(value);
|
|
96
|
+
}
|
|
97
|
+
/** The page address a record holds in its address field, or `''`. */ export function recordAddressOf(values, slugField) {
|
|
98
|
+
return normalizeRecordAddress(values == null ? void 0 : values[slugField]);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A base as stored: `/Services/Residential/` → `services/residential`. `null`
|
|
102
|
+
* when it is empty, has more than {@link RECORD_PAGE_BASE_MAX_SEGMENTS}
|
|
103
|
+
* segments, or a segment that is not URL-safe once lowercased — a base is
|
|
104
|
+
* typed by a person and refused rather than rewritten, because a rewritten
|
|
105
|
+
* base is a URL they did not choose.
|
|
106
|
+
*/ export function normalizeRecordPageBase(input) {
|
|
107
|
+
if (typeof input !== 'string') return null;
|
|
108
|
+
const segments = input.trim().toLowerCase().split('/').filter(Boolean);
|
|
109
|
+
if (!segments.length || segments.length > RECORD_PAGE_BASE_MAX_SEGMENTS) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
for (const segment of segments){
|
|
113
|
+
if (segment.length > RECORD_PAGE_BASE_SEGMENT_MAX || !SEGMENT.test(segment)) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return segments.join('/');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Why a base cannot be used on this site, as one sentence an author reads, or
|
|
121
|
+
* `null` when it can.
|
|
122
|
+
*
|
|
123
|
+
* Record pages are answered after every published page and before content
|
|
124
|
+
* collections, so a base whose first segment is a collection's would take
|
|
125
|
+
* that collection's entry addresses; the platform's reserved addresses and
|
|
126
|
+
* another plugin's routes are refused for the same reason. Two templates may
|
|
127
|
+
* nest — `services` and `services/residential` serve different depths — but
|
|
128
|
+
* never share a base.
|
|
129
|
+
*/ export function recordPageBaseRefusal(input, context = {}) {
|
|
130
|
+
var _context_pluginRouteSegments, _context_collectionSlugs, _context_otherBases;
|
|
131
|
+
const base = normalizeRecordPageBase(input);
|
|
132
|
+
if (!base) {
|
|
133
|
+
return `Use one to ${RECORD_PAGE_BASE_MAX_SEGMENTS} path segments of lowercase ` + 'letters, numbers and hyphens, like services or services/residential';
|
|
134
|
+
}
|
|
135
|
+
const [first] = base.split('/');
|
|
136
|
+
const reserved = reservedScreenRouteSegment(base);
|
|
137
|
+
if (reserved || first === AUTHOR_ROUTE_SEGMENT) {
|
|
138
|
+
return `/${reserved != null ? reserved : first} is a reserved address on every site — pick another base`;
|
|
139
|
+
}
|
|
140
|
+
if ((_context_pluginRouteSegments = context.pluginRouteSegments) == null ? void 0 : _context_pluginRouteSegments.includes(first)) {
|
|
141
|
+
return `/${first} already serves pages of its own on this site — pick another base`;
|
|
142
|
+
}
|
|
143
|
+
if ((_context_collectionSlugs = context.collectionSlugs) == null ? void 0 : _context_collectionSlugs.includes(first)) {
|
|
144
|
+
return `/${first} is a content collection on this site — pick another base`;
|
|
145
|
+
}
|
|
146
|
+
if ((_context_otherBases = context.otherBases) == null ? void 0 : _context_otherBases.includes(base)) {
|
|
147
|
+
return `Another record template already uses /${base}`;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
/** A record page's site path: `/services/kitchen-remodel`. */ export function recordPagePath(base, address) {
|
|
152
|
+
return `/${base}/${address}`;
|
|
153
|
+
}
|
|
154
|
+
/** The route a record template answers, as the page review names it. */ export function recordPageRoute(base) {
|
|
155
|
+
return `/${base}/:slug`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The binding a request path is a record page of, with the address it asks
|
|
159
|
+
* for — or `null`. A path matches a binding when it is the base plus exactly
|
|
160
|
+
* one more segment, so nested bases never compete: `/services/residential`
|
|
161
|
+
* asks `services` for `residential`, and `/services/residential/roofing` asks
|
|
162
|
+
* `services/residential` for `roofing`.
|
|
163
|
+
*
|
|
164
|
+
* The address segment is normalized the way a stored one is, so a request
|
|
165
|
+
* spelled `/services/Kitchen-Remodel` finds the record and the head names the
|
|
166
|
+
* one address that is canonical.
|
|
167
|
+
*/ export function matchRecordPagePath(bindings, path) {
|
|
168
|
+
const segments = path.split('/').filter(Boolean);
|
|
169
|
+
if (segments.length < 2) return null;
|
|
170
|
+
const base = segments.slice(0, -1).join('/').toLowerCase();
|
|
171
|
+
const binding = bindings.find((one)=>one.base === base);
|
|
172
|
+
if (!binding) return null;
|
|
173
|
+
const address = normalizeRecordAddress(safeDecode(segments[segments.length - 1]));
|
|
174
|
+
return address ? {
|
|
175
|
+
binding,
|
|
176
|
+
address
|
|
177
|
+
} : null;
|
|
178
|
+
}
|
|
179
|
+
function safeDecode(segment) {
|
|
180
|
+
try {
|
|
181
|
+
return decodeURIComponent(segment);
|
|
182
|
+
} catch (unused) {
|
|
183
|
+
return segment;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Is this field a page address? */ export function isRecordAddressField(field) {
|
|
187
|
+
return (field == null ? void 0 : field.type) === 'text' && field.customType === RECORD_PAGE_ADDRESS_FIELD_TYPE;
|
|
188
|
+
}
|
|
189
|
+
/** The model's page-address fields, in display order. */ export function recordAddressFieldIds(model) {
|
|
190
|
+
var _model_order;
|
|
191
|
+
// Tolerant of a partial model, as the filter fields' readers are: a write
|
|
192
|
+
// path must not fail over a model stored without its `order`.
|
|
193
|
+
return ((_model_order = model.order) != null ? _model_order : []).filter((fieldId)=>{
|
|
194
|
+
var _model_fields;
|
|
195
|
+
return isRecordAddressField((_model_fields = model.fields) == null ? void 0 : _model_fields[fieldId]);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Fills each EMPTY page-address field from the field it names as its source
|
|
200
|
+
* (`slugFrom`), and normalizes every address the values already hold.
|
|
201
|
+
*
|
|
202
|
+
* Run on a record's WHOLE values before every write — a create, an import, a
|
|
203
|
+
* form or automation append, and the merged values of an update — which is
|
|
204
|
+
* what makes "the address fills in once and never moves" true: an update that
|
|
205
|
+
* renames the record arrives with the address it already has, so there is
|
|
206
|
+
* nothing empty to fill. Returns the input itself when nothing changes.
|
|
207
|
+
*/ export function fillRecordAddresses(model, values) {
|
|
208
|
+
let next = null;
|
|
209
|
+
for (const fieldId of recordAddressFieldIds(model)){
|
|
210
|
+
var _model_fields;
|
|
211
|
+
const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[fieldId];
|
|
212
|
+
const held = values[fieldId];
|
|
213
|
+
const normalized = held == null || held === '' ? (field == null ? void 0 : field.slugFrom) ? normalizeRecordAddress(values[field.slugFrom]) : '' : normalizeRecordAddress(held);
|
|
214
|
+
if ((held != null ? held : '') === normalized) continue;
|
|
215
|
+
next != null ? next : next = _extends({}, values);
|
|
216
|
+
if (normalized) next[fieldId] = normalized;
|
|
217
|
+
else delete next[fieldId];
|
|
218
|
+
}
|
|
219
|
+
return next != null ? next : values;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Unique addresses for a set of records being given one for the first time —
|
|
223
|
+
* `kitchen-remodel`, `kitchen-remodel-2` — in the order given, skipping the
|
|
224
|
+
* addresses already `taken`. A record whose source has nothing addressable in
|
|
225
|
+
* it gets none.
|
|
226
|
+
*/ export function uniqueRecordAddresses(sources, taken = []) {
|
|
227
|
+
const used = new Set(taken);
|
|
228
|
+
const assigned = new Map();
|
|
229
|
+
for (const { id, source } of sources){
|
|
230
|
+
const base = normalizeRecordAddress(source);
|
|
231
|
+
if (!base) continue;
|
|
232
|
+
let candidate = base;
|
|
233
|
+
for(let suffix = 2; used.has(candidate); suffix += 1){
|
|
234
|
+
const tail = `-${suffix}`;
|
|
235
|
+
candidate = `${base.slice(0, RECORD_PAGE_ADDRESS_MAX - tail.length).replace(/-+$/g, '')}${tail}`;
|
|
236
|
+
}
|
|
237
|
+
used.add(candidate);
|
|
238
|
+
assigned.set(id, candidate);
|
|
239
|
+
}
|
|
240
|
+
return assigned;
|
|
241
|
+
}
|
|
242
|
+
const textOf = (value)=>{
|
|
243
|
+
if (typeof value === 'string') return value.trim();
|
|
244
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
245
|
+
return '';
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* The field that names a record page: the binding's `titleField`, else the
|
|
249
|
+
* model's first text field that is not a page address.
|
|
250
|
+
*/ export function recordPageNameField(binding, model) {
|
|
251
|
+
if (binding.titleField && model.fields[binding.titleField]) {
|
|
252
|
+
return binding.titleField;
|
|
253
|
+
}
|
|
254
|
+
return model.order.find((fieldId)=>{
|
|
255
|
+
const field = model.fields[fieldId];
|
|
256
|
+
return (field == null ? void 0 : field.type) === 'text' && fieldId !== binding.slugField && !isRecordAddressField(field);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The head of one record's page, from the fields the binding picked. Each
|
|
261
|
+
* part is absent when its field is unset or empty, and the head then falls
|
|
262
|
+
* back to the template page's own value, then the site's, exactly as it does
|
|
263
|
+
* for every page.
|
|
264
|
+
*/ export function recordPageHead(binding, model, values) {
|
|
265
|
+
const pick = (fieldId)=>fieldId ? textOf(values[fieldId]) : '';
|
|
266
|
+
const name = pick(recordPageNameField(binding, model));
|
|
267
|
+
const title = pick(binding.seoTitleField);
|
|
268
|
+
const description = pick(binding.seoDescriptionField);
|
|
269
|
+
const image = pick(binding.seoImageField);
|
|
270
|
+
return _extends({}, name ? {
|
|
271
|
+
name
|
|
272
|
+
} : {}, title ? {
|
|
273
|
+
title
|
|
274
|
+
} : {}, description ? {
|
|
275
|
+
description
|
|
276
|
+
} : {}, image ? {
|
|
277
|
+
image
|
|
278
|
+
} : {});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* A binding document read back, or `null` when it does not describe one: the
|
|
282
|
+
* fields every reader needs, trimmed. Optional fields that are blank are
|
|
283
|
+
* dropped rather than kept as `''`.
|
|
284
|
+
*/ export function parseRecordPageBinding(screenId, data) {
|
|
285
|
+
const text = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
286
|
+
const datasetId = text(data == null ? void 0 : data['datasetId']);
|
|
287
|
+
const base = normalizeRecordPageBase(data == null ? void 0 : data['base']);
|
|
288
|
+
const slugField = text(data == null ? void 0 : data['slugField']);
|
|
289
|
+
if (!screenId || !datasetId || !base || !slugField) return null;
|
|
290
|
+
const optional = (key)=>{
|
|
291
|
+
const value = text(data == null ? void 0 : data[key]);
|
|
292
|
+
return value ? {
|
|
293
|
+
[key]: value
|
|
294
|
+
} : {};
|
|
295
|
+
};
|
|
296
|
+
return _extends({
|
|
297
|
+
screenId,
|
|
298
|
+
datasetId,
|
|
299
|
+
base,
|
|
300
|
+
slugField
|
|
301
|
+
}, optional('titleField'), optional('seoTitleField'), optional('seoDescriptionField'), optional('seoImageField'));
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The records a row's reference fields point at, by the dataset each lives in:
|
|
305
|
+
* what a one-hop `{{item.author.name}}` on a record page has to read. A
|
|
306
|
+
* multiple reference contributes every id it holds.
|
|
307
|
+
*/ export function referencedRecordIds(model, record) {
|
|
308
|
+
var _model_references;
|
|
309
|
+
const wanted = new Map();
|
|
310
|
+
for (const [fieldId, datasetId] of Object.entries((_model_references = model.references) != null ? _model_references : {})){
|
|
311
|
+
if (!datasetId || datasetId.includes('/')) continue;
|
|
312
|
+
const value = record[fieldId];
|
|
313
|
+
for (const id of Array.isArray(value) ? value : [
|
|
314
|
+
value
|
|
315
|
+
]){
|
|
316
|
+
var _wanted_get;
|
|
317
|
+
if (typeof id !== 'string' || !id || id.includes('/')) continue;
|
|
318
|
+
const ids = (_wanted_get = wanted.get(datasetId)) != null ? _wanted_get : new Set();
|
|
319
|
+
ids.add(id);
|
|
320
|
+
wanted.set(datasetId, ids);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return wanted;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The row key `{{item.url}}` reads: a record's page on this site.
|
|
327
|
+
* Persisted in nothing — it is computed on every read — but bound by authors,
|
|
328
|
+
* so never rename.
|
|
329
|
+
*/ export const RECORD_PAGE_URL_KEY = 'url';
|
|
330
|
+
/**
|
|
331
|
+
* A dataset's repeated rows, each carrying its page as `url` when this site
|
|
332
|
+
* has a record template for the dataset (AGL-3475) — so a listing card links
|
|
333
|
+
* to the record's page with `{{item.url}}`, and the canvas previews the same
|
|
334
|
+
* link the page renders.
|
|
335
|
+
*
|
|
336
|
+
* A model that declares a field of its own called `url` keeps it: the token
|
|
337
|
+
* always means the dataset's field first. A row with no address has no page,
|
|
338
|
+
* and so no `url`, and its token stays as written. Returns the rows
|
|
339
|
+
* themselves when there is nothing to add.
|
|
340
|
+
*/ export function withRecordPageUrls(rows, binding, model) {
|
|
341
|
+
var _model_fields;
|
|
342
|
+
if (!binding || ((_model_fields = model.fields) == null ? void 0 : _model_fields[RECORD_PAGE_URL_KEY])) return rows;
|
|
343
|
+
return rows.map((row)=>{
|
|
344
|
+
const address = recordAddressOf(row, binding.slugField);
|
|
345
|
+
return address ? _extends({}, row, {
|
|
346
|
+
[RECORD_PAGE_URL_KEY]: recordPagePath(binding.base, address)
|
|
347
|
+
}) : row;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
//# sourceMappingURL=record-pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/record-pages/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/**\n * Record pages (AGL-3475): one page per dataset record, rendered from one\n * template page.\n *\n * A site marks a page as the RECORD TEMPLATE of a dataset and names a base\n * path, and every record of that dataset with a page address is served at\n * `/{base}/{address}` — a services dataset becomes `/services/kitchen-remodel`,\n * `/services/roofing`, and so on, all drawn by the one design. Inside the\n * template `{{item.field}}` means exactly what it means inside a repeat\n * (`substituteRecordTokens`, one reference hop), so an author who has built a\n * repeated card already knows how to build the page it links to.\n *\n * ## What lives where\n *\n * - The BINDING — which page, which dataset, which base, which fields — is a\n * document of this plugin's own, `hosts/{hostId}/recordPages/{screenId}`.\n * It is per site because the base and the page are; the dataset is the\n * organization's, so one dataset can have a record template on every site\n * it is shared with, each at its own base.\n * - The ADDRESS is a field on the record: a text field of the \"Page address\"\n * custom type ({@link RECORD_PAGE_ADDRESS_FIELD_TYPE}). It is stored rather\n * than derived at read time for two reasons. A stored address is what a\n * query can find — `filterValues.<field>` is already indexed on every record\n * (AGL-3321) — and it is what keeps a live URL where it is when somebody\n * renames the record. An address field may name a SOURCE field\n * (`slugFrom`): an empty address fills in from it once, on whichever write\n * first finds it empty, and never moves afterwards.\n * - The TEMPLATE is an ordinary screen stamped `kind: 'template'`, the stamp a\n * blog's entry template carries: it renders at no address of its own, it is\n * not one of the plan's pages, and its raw `{{item.*}}` never ships.\n *\n * Pure and SDK-free: the console's settings, the canvas preview and the\n * tenant's resolver all read it.\n */\n\nimport { AUTHOR_ROUTE_SEGMENT } from '@aglyn/aglyn/app-utils/content-authors'\nimport { reservedScreenRouteSegment } from '@aglyn/aglyn/app-utils/screen-route'\nimport { urlSlugSegment } from '@aglyn/aglyn/app-utils/url-slug'\nimport type { RepeatRowsModel } from '@aglyn/aglyn/app-utils/expand-repeatables'\nimport type { CustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport type { DatasetFieldDefinition, DatasetModel } from '../model/dataset-models'\n\n/** This plugin's host subcollection the bindings live in. Persisted — never rename. */\nexport const DATASET_RECORD_PAGES_COLLECTION = 'recordPages'\n\n/**\n * The custom field type a record's page address rides (`text` storage).\n * Persisted on field definitions — never rename.\n */\nexport const RECORD_PAGE_ADDRESS_FIELD_TYPE = 'pageAddress'\n\n/**\n * The longest page address, in characters. It is the length a plain text\n * value's `filterValues` entry is clipped to (`DATASET_FILTER_VALUE_MAX`), so\n * every address a record can hold is one the indexed lookup can find whole.\n */\nexport const RECORD_PAGE_ADDRESS_MAX = 64\n\n/** The most path segments a base may have: `services/residential/roofing`. */\nexport const RECORD_PAGE_BASE_MAX_SEGMENTS = 4\n\n/** The longest single base segment. */\nexport const RECORD_PAGE_BASE_SEGMENT_MAX = 60\n\n/** How many record templates one site may hold. */\nexport const RECORD_PAGES_MAX_PER_SITE = 50\n\n/** One URL-safe segment: lowercase letters and digits joined by single hyphens. */\nconst SEGMENT = /^[a-z0-9]+(?:-[a-z0-9]+)*$/\n\n/**\n * A site's record template, as `hosts/{hostId}/recordPages/{screenId}` holds\n * it. The document id is the template screen's id, so a page is the template\n * of at most one dataset.\n */\nexport interface DatasetRecordPageBinding {\n /** The template screen — also the document id. */\n screenId: string\n /** The organization dataset whose records the pages render. */\n datasetId: string\n /**\n * The path the pages are served under, without slashes at either end: one\n * or more segments (`services`, `services/residential`).\n */\n base: string\n /** The record's page-address field. */\n slugField: string\n /**\n * The field that NAMES a record page — the tab title's first half, the\n * breadcrumb, the sitemap's view of it. Absent, the dataset's first text\n * field does.\n */\n titleField?: string\n /** A field whose value is the search title, used verbatim. */\n seoTitleField?: string\n /** A field whose value is the search description. */\n seoDescriptionField?: string\n /** A field holding the sharing image: a media reference or a URL. */\n seoImageField?: string\n}\n\n/**\n * A record's page address from any text: the platform's URL segment\n * (`urlSlugSegment`, the rule category and author pages use) of the text with\n * its accents folded first, so `Café` is `cafe` rather than `caf`, and at most\n * {@link RECORD_PAGE_ADDRESS_MAX} characters. Idempotent, so an address that\n * is already one comes back unchanged — `Kitchen Remodeling` →\n * `kitchen-remodeling` → `kitchen-remodeling`. `''` for text with nothing\n * addressable in it.\n */\nexport function normalizeRecordAddress(value: unknown): string {\n if (typeof value !== 'string' && typeof value !== 'number') return ''\n return urlSlugSegment(\n String(value).normalize('NFKD').replace(/[̀-ͯ]/g, ''),\n )\n .slice(0, RECORD_PAGE_ADDRESS_MAX)\n .replace(/-+$/g, '')\n}\n\n/**\n * The \"Page address\" field type, as the custom-field registry holds it: text\n * storage, offered in the schema dialog, and checked on every write path\n * after the address has been normalized (`fillRecordAddresses`), so what it\n * refuses is an address with nothing addressable left in it, never one typed\n * with a capital letter.\n */\nexport const RECORD_PAGE_ADDRESS_FIELD: CustomFieldType = {\n name: RECORD_PAGE_ADDRESS_FIELD_TYPE,\n pluginId: BUNDLE_ID,\n label: 'Page address',\n baseType: 'text',\n description:\n 'The record’s own page under a record template: lowercase words joined by hyphens',\n validate: (value) =>\n isRecordAddress(value)\n ? null\n : `Use lowercase letters, numbers and hyphens, up to ${RECORD_PAGE_ADDRESS_MAX} characters`,\n}\n\n/** Whether a value is already a page address, exactly as stored. */\nexport function isRecordAddress(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length <= RECORD_PAGE_ADDRESS_MAX &&\n SEGMENT.test(value)\n )\n}\n\n/** The page address a record holds in its address field, or `''`. */\nexport function recordAddressOf(\n values: Record<string, unknown> | null | undefined,\n slugField: string,\n): string {\n return normalizeRecordAddress(values?.[slugField])\n}\n\n/**\n * A base as stored: `/Services/Residential/` → `services/residential`. `null`\n * when it is empty, has more than {@link RECORD_PAGE_BASE_MAX_SEGMENTS}\n * segments, or a segment that is not URL-safe once lowercased — a base is\n * typed by a person and refused rather than rewritten, because a rewritten\n * base is a URL they did not choose.\n */\nexport function normalizeRecordPageBase(input: unknown): string | null {\n if (typeof input !== 'string') return null\n const segments = input\n .trim()\n .toLowerCase()\n .split('/')\n .filter(Boolean)\n if (!segments.length || segments.length > RECORD_PAGE_BASE_MAX_SEGMENTS) {\n return null\n }\n for (const segment of segments) {\n if (segment.length > RECORD_PAGE_BASE_SEGMENT_MAX || !SEGMENT.test(segment)) {\n return null\n }\n }\n return segments.join('/')\n}\n\n/** What a base must not collide with, on the site it is chosen for. */\nexport interface RecordPageBaseContext {\n /** The site's content collections' slugs, which own `/{slug}/…`. */\n collectionSlugs?: readonly string[]\n /**\n * The first segments other plugins serve pages under on every site —\n * `products` for a store — read off their declared sitemap sections.\n */\n pluginRouteSegments?: readonly string[]\n /** The bases the site's OTHER record templates already use. */\n otherBases?: readonly string[]\n}\n\n/**\n * Why a base cannot be used on this site, as one sentence an author reads, or\n * `null` when it can.\n *\n * Record pages are answered after every published page and before content\n * collections, so a base whose first segment is a collection's would take\n * that collection's entry addresses; the platform's reserved addresses and\n * another plugin's routes are refused for the same reason. Two templates may\n * nest — `services` and `services/residential` serve different depths — but\n * never share a base.\n */\nexport function recordPageBaseRefusal(\n input: unknown,\n context: RecordPageBaseContext = {},\n): string | null {\n const base = normalizeRecordPageBase(input)\n if (!base) {\n return (\n `Use one to ${RECORD_PAGE_BASE_MAX_SEGMENTS} path segments of lowercase ` +\n 'letters, numbers and hyphens, like services or services/residential'\n )\n }\n const [first] = base.split('/')\n const reserved = reservedScreenRouteSegment(base)\n if (reserved || first === AUTHOR_ROUTE_SEGMENT) {\n return `/${reserved ?? first} is a reserved address on every site — pick another base`\n }\n if (context.pluginRouteSegments?.includes(first)) {\n return `/${first} already serves pages of its own on this site — pick another base`\n }\n if (context.collectionSlugs?.includes(first)) {\n return `/${first} is a content collection on this site — pick another base`\n }\n if (context.otherBases?.includes(base)) {\n return `Another record template already uses /${base}`\n }\n return null\n}\n\n/** A record page's site path: `/services/kitchen-remodel`. */\nexport function recordPagePath(base: string, address: string): string {\n return `/${base}/${address}`\n}\n\n/** The route a record template answers, as the page review names it. */\nexport function recordPageRoute(base: string): string {\n return `/${base}/:slug`\n}\n\n/**\n * The binding a request path is a record page of, with the address it asks\n * for — or `null`. A path matches a binding when it is the base plus exactly\n * one more segment, so nested bases never compete: `/services/residential`\n * asks `services` for `residential`, and `/services/residential/roofing` asks\n * `services/residential` for `roofing`.\n *\n * The address segment is normalized the way a stored one is, so a request\n * spelled `/services/Kitchen-Remodel` finds the record and the head names the\n * one address that is canonical.\n */\nexport function matchRecordPagePath<B extends Pick<DatasetRecordPageBinding, 'base'>>(\n bindings: readonly B[],\n path: string,\n): { binding: B; address: string } | null {\n const segments = path.split('/').filter(Boolean)\n if (segments.length < 2) return null\n const base = segments.slice(0, -1).join('/').toLowerCase()\n const binding = bindings.find((one) => one.base === base)\n if (!binding) return null\n const address = normalizeRecordAddress(\n safeDecode(segments[segments.length - 1]),\n )\n return address ? { binding, address } : null\n}\n\nfunction safeDecode(segment: string): string {\n try {\n return decodeURIComponent(segment)\n } catch {\n return segment\n }\n}\n\n/** Is this field a page address? */\nexport function isRecordAddressField(\n field: DatasetFieldDefinition | null | undefined,\n): boolean {\n return field?.type === 'text' && field.customType === RECORD_PAGE_ADDRESS_FIELD_TYPE\n}\n\n/** The model's page-address fields, in display order. */\nexport function recordAddressFieldIds(model: DatasetModel): string[] {\n // Tolerant of a partial model, as the filter fields' readers are: a write\n // path must not fail over a model stored without its `order`.\n return (model.order ?? []).filter((fieldId) =>\n isRecordAddressField(model.fields?.[fieldId]),\n )\n}\n\n/**\n * Fills each EMPTY page-address field from the field it names as its source\n * (`slugFrom`), and normalizes every address the values already hold.\n *\n * Run on a record's WHOLE values before every write — a create, an import, a\n * form or automation append, and the merged values of an update — which is\n * what makes \"the address fills in once and never moves\" true: an update that\n * renames the record arrives with the address it already has, so there is\n * nothing empty to fill. Returns the input itself when nothing changes.\n */\nexport function fillRecordAddresses<V extends Record<string, unknown>>(\n model: DatasetModel,\n values: V,\n): V {\n let next: V | null = null\n for (const fieldId of recordAddressFieldIds(model)) {\n const field = model.fields?.[fieldId]\n const held = values[fieldId]\n const normalized =\n held == null || held === ''\n ? field?.slugFrom\n ? normalizeRecordAddress(values[field.slugFrom])\n : ''\n : normalizeRecordAddress(held)\n if ((held ?? '') === normalized) continue\n next ??= { ...values }\n if (normalized) (next as Record<string, unknown>)[fieldId] = normalized\n else delete (next as Record<string, unknown>)[fieldId]\n }\n return next ?? values\n}\n\n/**\n * Unique addresses for a set of records being given one for the first time —\n * `kitchen-remodel`, `kitchen-remodel-2` — in the order given, skipping the\n * addresses already `taken`. A record whose source has nothing addressable in\n * it gets none.\n */\nexport function uniqueRecordAddresses(\n sources: ReadonlyArray<{ id: string; source: unknown }>,\n taken: Iterable<string> = [],\n): Map<string, string> {\n const used = new Set(taken)\n const assigned = new Map<string, string>()\n for (const { id, source } of sources) {\n const base = normalizeRecordAddress(source)\n if (!base) continue\n let candidate = base\n for (let suffix = 2; used.has(candidate); suffix += 1) {\n const tail = `-${suffix}`\n candidate = `${base.slice(0, RECORD_PAGE_ADDRESS_MAX - tail.length).replace(/-+$/g, '')}${tail}`\n }\n used.add(candidate)\n assigned.set(id, candidate)\n }\n return assigned\n}\n\nconst textOf = (value: unknown): string => {\n if (typeof value === 'string') return value.trim()\n if (typeof value === 'number' && Number.isFinite(value)) return String(value)\n return ''\n}\n\n/**\n * The field that names a record page: the binding's `titleField`, else the\n * model's first text field that is not a page address.\n */\nexport function recordPageNameField(\n binding: Pick<DatasetRecordPageBinding, 'titleField' | 'slugField'>,\n model: DatasetModel,\n): string | undefined {\n if (binding.titleField && model.fields[binding.titleField]) {\n return binding.titleField\n }\n return model.order.find((fieldId) => {\n const field = model.fields[fieldId]\n return field?.type === 'text' && fieldId !== binding.slugField && !isRecordAddressField(field)\n })\n}\n\n/** What a record page says about itself in the head. */\nexport interface RecordPageHead {\n /** The page's name: the site title is joined after it. */\n name?: string\n /** An authored search title, used verbatim. */\n title?: string\n description?: string\n /** A media reference or URL, resolved by the head like any page image. */\n image?: string\n}\n\n/**\n * The head of one record's page, from the fields the binding picked. Each\n * part is absent when its field is unset or empty, and the head then falls\n * back to the template page's own value, then the site's, exactly as it does\n * for every page.\n */\nexport function recordPageHead(\n binding: DatasetRecordPageBinding,\n model: DatasetModel,\n values: Record<string, unknown>,\n): RecordPageHead {\n const pick = (fieldId: string | undefined) =>\n fieldId ? textOf(values[fieldId]) : ''\n const name = pick(recordPageNameField(binding, model))\n const title = pick(binding.seoTitleField)\n const description = pick(binding.seoDescriptionField)\n const image = pick(binding.seoImageField)\n return {\n ...(name ? { name } : {}),\n ...(title ? { title } : {}),\n ...(description ? { description } : {}),\n ...(image ? { image } : {}),\n }\n}\n\n/**\n * A binding document read back, or `null` when it does not describe one: the\n * fields every reader needs, trimmed. Optional fields that are blank are\n * dropped rather than kept as `''`.\n */\nexport function parseRecordPageBinding(\n screenId: string,\n data: Record<string, unknown> | null | undefined,\n): DatasetRecordPageBinding | null {\n const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n const datasetId = text(data?.['datasetId'])\n const base = normalizeRecordPageBase(data?.['base'])\n const slugField = text(data?.['slugField'])\n if (!screenId || !datasetId || !base || !slugField) return null\n const optional = (key: keyof DatasetRecordPageBinding) => {\n const value = text(data?.[key])\n return value ? { [key]: value } : {}\n }\n return {\n screenId,\n datasetId,\n base,\n slugField,\n ...optional('titleField'),\n ...optional('seoTitleField'),\n ...optional('seoDescriptionField'),\n ...optional('seoImageField'),\n }\n}\n\n/**\n * The records a row's reference fields point at, by the dataset each lives in:\n * what a one-hop `{{item.author.name}}` on a record page has to read. A\n * multiple reference contributes every id it holds.\n */\nexport function referencedRecordIds(\n model: RepeatRowsModel,\n record: Record<string, unknown>,\n): Map<string, Set<string>> {\n const wanted = new Map<string, Set<string>>()\n for (const [fieldId, datasetId] of Object.entries(model.references ?? {})) {\n if (!datasetId || datasetId.includes('/')) continue\n const value = record[fieldId]\n for (const id of Array.isArray(value) ? value : [value]) {\n if (typeof id !== 'string' || !id || id.includes('/')) continue\n const ids = wanted.get(datasetId) ?? new Set<string>()\n ids.add(id)\n wanted.set(datasetId, ids)\n }\n }\n return wanted\n}\n\n/**\n * The row key `{{item.url}}` reads: a record's page on this site.\n * Persisted in nothing — it is computed on every read — but bound by authors,\n * so never rename.\n */\nexport const RECORD_PAGE_URL_KEY = 'url'\n\n/**\n * A dataset's repeated rows, each carrying its page as `url` when this site\n * has a record template for the dataset (AGL-3475) — so a listing card links\n * to the record's page with `{{item.url}}`, and the canvas previews the same\n * link the page renders.\n *\n * A model that declares a field of its own called `url` keeps it: the token\n * always means the dataset's field first. A row with no address has no page,\n * and so no `url`, and its token stays as written. Returns the rows\n * themselves when there is nothing to add.\n */\nexport function withRecordPageUrls<R extends Record<string, unknown>>(\n rows: R[],\n binding: Pick<DatasetRecordPageBinding, 'base' | 'slugField'> | null | undefined,\n model: DatasetModel,\n): R[] {\n if (!binding || model.fields?.[RECORD_PAGE_URL_KEY]) return rows\n return rows.map((row) => {\n const address = recordAddressOf(row, binding.slugField)\n return address\n ? { ...row, [RECORD_PAGE_URL_KEY]: recordPagePath(binding.base, address) }\n : row\n })\n}\n"],"names":["AUTHOR_ROUTE_SEGMENT","reservedScreenRouteSegment","urlSlugSegment","BUNDLE_ID","DATASET_RECORD_PAGES_COLLECTION","RECORD_PAGE_ADDRESS_FIELD_TYPE","RECORD_PAGE_ADDRESS_MAX","RECORD_PAGE_BASE_MAX_SEGMENTS","RECORD_PAGE_BASE_SEGMENT_MAX","RECORD_PAGES_MAX_PER_SITE","SEGMENT","normalizeRecordAddress","value","String","normalize","replace","slice","RECORD_PAGE_ADDRESS_FIELD","name","pluginId","label","baseType","description","validate","isRecordAddress","length","test","recordAddressOf","values","slugField","normalizeRecordPageBase","input","segments","trim","toLowerCase","split","filter","Boolean","segment","join","recordPageBaseRefusal","context","base","first","reserved","pluginRouteSegments","includes","collectionSlugs","otherBases","recordPagePath","address","recordPageRoute","matchRecordPagePath","bindings","path","binding","find","one","safeDecode","decodeURIComponent","isRecordAddressField","field","type","customType","recordAddressFieldIds","model","order","fieldId","fields","fillRecordAddresses","next","held","normalized","slugFrom","uniqueRecordAddresses","sources","taken","used","Set","assigned","Map","id","source","candidate","suffix","has","tail","add","set","textOf","Number","isFinite","recordPageNameField","titleField","recordPageHead","pick","title","seoTitleField","seoDescriptionField","image","seoImageField","parseRecordPageBinding","screenId","data","text","datasetId","optional","key","referencedRecordIds","record","wanted","Object","entries","references","Array","isArray","ids","get","RECORD_PAGE_URL_KEY","withRecordPageUrls","rows","map","row"],"mappings":"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"}
|