@uniweb/core 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
package/src/block.js CHANGED
@@ -87,7 +87,7 @@ export default class Block {
87
87
 
88
88
  // Content structure
89
89
  // The content can be:
90
- // 1. Raw ProseMirror content (from content collection)
90
+ // 1. Raw ProseMirror content (from a record)
91
91
  // 2. Pre-parsed content with main/items structure
92
92
  // For now, store raw and parse on demand
93
93
  //
@@ -358,7 +358,7 @@ export default class Block {
358
358
  *
359
359
  * Supports multiple input shapes:
360
360
  * 1. Pre-parsed groups structure (from the editor)
361
- * 2. ProseMirror document (from markdown collection)
361
+ * 2. ProseMirror document (from a markdown record)
362
362
  * 3. Wrapped ProseMirror document (content-API format)
363
363
  * 4. Plain object (passed through directly)
364
364
  *
package/src/data-paths.js CHANGED
@@ -1,10 +1,15 @@
1
1
  /**
2
- * Compiled-collection paths — the ONE home for the URL and directory
3
- * convention that the build emits and every fetcher requests.
2
+ * Compiled-query paths — the ONE home for the URL and directory convention that
3
+ * the build emits and every fetcher requests.
4
+ *
5
+ * ⚠️ `<name>` IS A QUERY'S NAME. `/data/<name>.json` is a named query's
6
+ * MATERIALIZATION — the answer when no host declares a live lane — and never the
7
+ * definition of anything. The records themselves live in `entities/{schema}/`,
8
+ * and `records.yml` decides which of them are published.
4
9
  *
5
10
  * Why this module exists. The path `/data/<name>.json` was a bare string
6
11
  * literal in six places across three packages: the build wrote it
7
- * (`collection-processor.js`), the build resolved `collection:` to it
12
+ * (the query processor), the build resolved the query shorthand to it
8
13
  * (`data-fetcher.js`), core injected the per-record default
9
14
  * (`fetch-config.js applyDeferredDetail`), core gated locale-prefixing on it
10
15
  * (`fetch-config.js localizeConfig`), the dev server matched it with a regex
@@ -27,11 +32,11 @@
27
32
  * inconsistency invites a rename. It has been proposed and declined. Those
28
33
  * are machinery: an endpoint and bundler artifacts, which no visitor should
29
34
  * land on and which an underscore correctly marks as internal. Compiled
30
- * collection JSON is the opposite — it is the site's own content, the same
35
+ * this JSON is the opposite — it is the site's own content, the same
31
36
  * records an agent that found the site through `llms.txt` may reasonably
32
37
  * fetch directly. `/data/articles.json` is a legitimate public address, and
33
38
  * `data` is a legitimate page route; neither collides with the other, since
34
- * pages emit `.html` and collections emit `.json`. Prefixing it would say
39
+ * pages emit `.html` and queries emit `.json`. Prefixing it would say
35
40
  * "internal" about something that is not.
36
41
  *
37
42
  * Zero-dependency leaf, like `./locale-config.js`, so a consumer that must
@@ -47,26 +52,26 @@
47
52
  export const DATA_DIR = 'data'
48
53
 
49
54
  /**
50
- * The URL prefix every compiled-collection request carries. Note the
55
+ * The URL prefix every compiled-query request carries. Note the
51
56
  * trailing slash: `isDataUrl` is a prefix test, and without it `/database`
52
57
  * would match.
53
58
  */
54
59
  export const DATA_URL_PREFIX = `/${DATA_DIR}/`
55
60
 
56
61
  /**
57
- * URL of a collection's cascade payload — the whole collection, with
58
- * `deferred:` fields stripped when the collection declares them.
62
+ * URL of a query's cascade payload — every record it returns, with
63
+ * `deferred:` fields stripped when the query declares them.
59
64
  *
60
- * @param {string} name - The collection name.
65
+ * @param {string} name - The query name.
61
66
  * @returns {string} e.g. `/data/articles.json`
62
67
  */
63
- export function collectionDataUrl(name) {
68
+ export function queryDataUrl(name) {
64
69
  return `${DATA_URL_PREFIX}${name}.json`
65
70
  }
66
71
 
67
72
  /**
68
73
  * URL of one record's full payload — every field, including deferred ones.
69
- * Emitted per item only when the collection declares `deferred:`.
74
+ * Emitted per record only when the query declares `deferred:`.
70
75
  *
71
76
  * Takes either a concrete slug (kit's `useEntityDetail`, which holds a
72
77
  * record) or the literal placeholder `{slug}` (core's `applyDeferredDetail`,
@@ -75,21 +80,21 @@ export function collectionDataUrl(name) {
75
80
  * function does not encode, matching the behavior of the call sites it
76
81
  * replaced.
77
82
  *
78
- * @param {string} collection - The collection name.
83
+ * @param {string} query - The query name.
79
84
  * @param {string} slug - A record slug, or a `{param}` placeholder.
80
85
  * @returns {string} e.g. `/data/articles/design-tips.json`
81
86
  */
82
- export function recordDataUrl(collection, slug) {
83
- return `${DATA_URL_PREFIX}${collection}/${slug}.json`
87
+ export function recordDataUrl(query, slug) {
88
+ return `${DATA_URL_PREFIX}${query}/${slug}.json`
84
89
  }
85
90
 
86
91
  /**
87
- * The inverse of `collectionDataUrl` — recover a collection name from a fetch
88
- * path so a caller can look it up among the declared collections.
92
+ * The inverse of `queryDataUrl` — recover a query name from a fetch
93
+ * path so a caller can look it up among the declared queries.
89
94
  *
90
95
  * Best-effort by design, and the caller decides what a miss means: a path
91
- * outside the compiled-collection tree is returned with only its `.json`
92
- * suffix removed, which simply will not match any declared collection and
96
+ * outside the compiled tree is returned with only its `.json`
97
+ * suffix removed, which simply will not match any declared query and
93
98
  * lets the caller fall through to reading the file. Nested names round-trip
94
99
  * (`/data/archive/2024/posts.json` → `archive/2024/posts`).
95
100
  *
@@ -98,16 +103,16 @@ export function recordDataUrl(collection, slug) {
98
103
  * how `validate-data.js` came to hold a sixth copy of it.
99
104
  *
100
105
  * @param {string} path - A fetch config's `path`.
101
- * @returns {string} The derived collection name.
106
+ * @returns {string} The derived query name.
102
107
  */
103
- export function collectionNameFromUrl(path) {
108
+ export function queryNameFromUrl(path) {
104
109
  if (typeof path !== 'string') return ''
105
110
  // DATA_DIR is a plain identifier segment, so it needs no regex escaping.
106
111
  return path.replace(new RegExp(`^/?${DATA_DIR}/`), '').replace(/\.json$/i, '')
107
112
  }
108
113
 
109
114
  /**
110
- * Whether a fetch config's `path` addresses compiled collection data.
115
+ * Whether a fetch config's `path` addresses compiled query data.
111
116
  *
112
117
  * Used to scope behavior that only makes sense for build-emitted files —
113
118
  * locale prefixing in particular, which must not touch a remote `url:`
package/src/detail-url.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Detail-record resolution — the ONE home for turning a collection's `detail:`
2
+ * Detail-record resolution — the ONE home for turning a query's `detail:`
3
3
  * declaration plus a dynamic route's params into a fetch config.
4
4
  *
5
5
  * Why this is a subpath rather than an EntityStore internal. A host that
@@ -47,31 +47,31 @@ function paramContext(paramName, paramValue) {
47
47
  }
48
48
 
49
49
  /**
50
- * Build a detail-URL fetch config from a collection config + dynamic context.
50
+ * Build a detail-URL fetch config from a query config + dynamic context.
51
51
  *
52
52
  * Four forms of `detail:`:
53
53
  * - `'rest'` — append paramValue as a path segment.
54
54
  * - `'query'` — append `?paramName=paramValue`.
55
55
  * - `'/articles/{slug}'` — custom URL pattern with {paramName} placeholders,
56
56
  * or the generic `{param}` alias (see below).
57
- * - `{ body, envelope }` — object form. Reuses the collection's url /
57
+ * - `{ body, envelope }` — object form. Reuses the query's url /
58
58
  * method / headers / auth; adds per-detail
59
59
  * body (with placeholder substitution) and
60
60
  * per-detail envelope.
61
61
  *
62
- * Returns `null` — never throws — when the collection declares no `detail:`,
63
- * when the dynamic context carries no param, or when the collection has
62
+ * Returns `null` — never throws — when the query declares no `detail:`,
63
+ * when the dynamic context carries no param, or when the query has
64
64
  * neither `url:` nor `path:` to build from. A caller treats `null` as "this
65
- * collection has no separate detail fetch", which is the common case.
65
+ * query has no separate detail fetch", which is the common case.
66
66
  *
67
- * @param {Object} collectionConfig - A resolved fetch config for the collection
67
+ * @param {Object} queryConfig - A resolved fetch config for the query
68
68
  * (post-`resolveFetchConfigs`, so `detail` may have been auto-injected for a
69
- * `deferred:` collection — see `./fetch-config.js`).
69
+ * `deferred:` query — see `./fetch-config.js`).
70
70
  * @param {{ paramName: string, paramValue: string }} dynamicContext
71
71
  * @returns {Object|null} A fetch config carrying `url` or `path`, or null.
72
72
  */
73
- export function buildDetailConfig(collectionConfig, dynamicContext) {
74
- const { detail } = collectionConfig
73
+ export function buildDetailConfig(queryConfig, dynamicContext) {
74
+ const { detail } = queryConfig
75
75
  if (!detail) return null
76
76
  const { paramName, paramValue } = dynamicContext
77
77
  if (!paramName || paramValue === undefined) return null
@@ -80,34 +80,52 @@ export function buildDetailConfig(collectionConfig, dynamicContext) {
80
80
  // kind: an `endpoint` carries remote semantics the fetcher decides on, so
81
81
  // returning a detail as `path` would silently drop operator pushdown and the
82
82
  // site's static headers for exactly the request that is one record.
83
- const baseUrl = collectionConfig.endpoint || collectionConfig.url || collectionConfig.path
83
+ const baseUrl = queryConfig.endpoint || queryConfig.url || queryConfig.path
84
84
  if (!baseUrl) return null
85
- const addressKey = collectionConfig.endpoint
85
+ const addressKey = queryConfig.endpoint
86
86
  ? 'endpoint'
87
- : collectionConfig.url
87
+ : queryConfig.url
88
88
  ? 'url'
89
89
  : 'path'
90
90
 
91
- // Object form: `detail: { body, envelope }`. Reuses collection's URL +
91
+ // Object form: `detail: { body, envelope }`. Reuses the query's URL +
92
92
  // method + headers + auth. The body is placeholder-substituted against
93
93
  // the dynamic context so `body: { variables: { slug: "{slug}" } }` works.
94
94
  if (detail && typeof detail === 'object') {
95
95
  const out = {
96
96
  [addressKey]: baseUrl,
97
- schema: collectionConfig.schema,
98
- transform: collectionConfig.transform,
97
+ schema: queryConfig.schema,
98
+ transform: queryConfig.transform,
99
99
  }
100
- if (collectionConfig.method) out.method = collectionConfig.method
100
+ if (queryConfig.method) out.method = queryConfig.method
101
101
  if (detail.body !== undefined) {
102
102
  out.body = substitutePlaceholders(detail.body, paramContext(paramName, paramValue), { encode: false })
103
- } else if (collectionConfig.body !== undefined) {
104
- out.body = substitutePlaceholders(collectionConfig.body, paramContext(paramName, paramValue), { encode: false })
103
+ } else if (queryConfig.body !== undefined) {
104
+ out.body = substitutePlaceholders(queryConfig.body, paramContext(paramName, paramValue), { encode: false })
105
105
  }
106
106
  if (detail.envelope) out.envelope = detail.envelope
107
107
  return out
108
108
  }
109
109
 
110
110
  // String-form: URL-based conventions.
111
+ //
112
+ // ⭐ `rest` and `query` BUILD FROM THE LIST URL, so its query string survives onto
113
+ // the detail request. That is deliberate and it is the safe default: the params
114
+ // that matter most to a single-record read are exactly the ones a list carries —
115
+ // `?lang=`, an API key, a tenancy id. Dropping them would 401 the detail request
116
+ // or return the wrong language, on every detail page.
117
+ //
118
+ // ⚠️ The cost is real and lands on ONE category: a PROJECTION param (`?fields=`,
119
+ // `?select=`) asks the API for a summary, and carrying it truncates the very
120
+ // record the detail fetch exists to get in full. The request still succeeds and
121
+ // only some fields are missing, so it reads as a component or API fault rather
122
+ // than a URL one.
123
+ //
124
+ // ⛔ Framework cannot tell the categories apart — they are the host's vocabulary,
125
+ // not ours. So the default keeps everything and the CUSTOM PATTERN form is the
126
+ // way out: it is used verbatim, so nothing carries over unless the author writes
127
+ // it. Documented for authors in `docs/reference/dynamic-routes.md` § *The list's
128
+ // query string carries over*.
111
129
  let detailUrl
112
130
  if (detail === 'rest') {
113
131
  const [basePath, queryString] = baseUrl.split('?')
@@ -127,7 +145,7 @@ export function buildDetailConfig(collectionConfig, dynamicContext) {
127
145
 
128
146
  return {
129
147
  [addressKey]: detailUrl,
130
- schema: collectionConfig.schema,
131
- transform: collectionConfig.transform,
148
+ schema: queryConfig.schema,
149
+ transform: queryConfig.transform,
132
150
  }
133
151
  }
@@ -5,7 +5,7 @@
5
5
  * asks the Website's FetcherDispatcher to execute them, and assembles the
6
6
  * data payload passed to `prepare-props`.
7
7
  *
8
- * The cascade, localization, detail-query handling, and collection-first
8
+ * The cascade, localization, detail-query handling, and list-first
9
9
  * content-gate logic all live here — unchanged from the pre-refactor model.
10
10
  * What changed: EntityStore no longer talks to DataStore directly. It calls
11
11
  * `website.fetcher.peek(request, ctx)` for the sync path (resolve) and
@@ -127,9 +127,12 @@ export default class EntityStore {
127
127
  schemas: requested,
128
128
  locale: website?.getActiveLocale?.() ?? null,
129
129
  defaultLocale: website?.getDefaultLocale?.() ?? null,
130
- collections: website?.config?.collections ?? null,
131
- // A host's live-collection lane. Absent on every static site and on
132
- // local dev, which is why `resolveCollectionSource` treats absence as
130
+ // ⚠️ `queries`, matching `resolveFetchConfigs`. This passed `collections`
131
+ // after the payload key was renamed a dead option name, silently: the
132
+ // resolver simply saw no queries and stopped injecting `detail:`.
133
+ queries: website?.config?.queries ?? null,
134
+ // A host's live-records lane. Absent on every static site and on
135
+ // local dev, which is why `resolveQuerySource` treats absence as
133
136
  // the ordinary case and reads the compiled artifact without comment.
134
137
  records: website?.config?.records ?? null,
135
138
  },
@@ -137,12 +140,12 @@ export default class EntityStore {
137
140
  }
138
141
 
139
142
  /**
140
- * Post-process assembled collection data: for each fetch config that declares a
143
+ * Post-process assembled records: for each fetch config that declares a
141
144
  * `detailPage` page-ref, resolve it to a locale route template (O(1), via the
142
145
  * Website's `_pageIdMap`) and inject a `route` on each record — so a dynamic-list
143
- * card links to the collection's canonical detail page regardless of which page
146
+ * card links to the query's canonical detail page regardless of which page
144
147
  * the list sits on. Runs after `data` is fully assembled, in BOTH the sync (peek)
145
- * and async (fetch) paths. Replaces the old runtime `getCollectionDetailRoute`
148
+ * and async (fetch) paths. Replaces the old runtime `getQueryDetailRoute`
146
149
  * page-tree scan. A dangling `detailPage` (unresolvable ref) is a no-op — the
147
150
  * component degrades gracefully; records with a baked `route` (file lane) are kept.
148
151
  */
@@ -159,14 +162,14 @@ export default class EntityStore {
159
162
  }
160
163
 
161
164
  /**
162
- * Build a detail-URL fetch config from a collection config + dynamic context.
165
+ * Build a detail-URL fetch config from a query config + dynamic context.
163
166
  *
164
167
  * Delegates to the exported resolver so a host fetching this record
165
168
  * server-side reaches the identical rule — see `./detail-url.js` for why the
166
169
  * four `detail:` forms are a contract rather than an implementation detail.
167
170
  */
168
- _buildDetailConfig(collectionConfig, dynamicContext) {
169
- return buildDetailConfig(collectionConfig, dynamicContext)
171
+ _buildDetailConfig(queryConfig, dynamicContext) {
172
+ return buildDetailConfig(queryConfig, dynamicContext)
170
173
  }
171
174
 
172
175
  /**
@@ -219,9 +222,9 @@ export default class EntityStore {
219
222
  const routeSchema = dynamicContext?.schema
220
223
 
221
224
  for (const [schema, cfg] of configs) {
222
- const isRouteCollection = dynamicContext && schema === routeSchema
223
- if (isRouteCollection && !inheritDetail) {
224
- // refine detail:false — the collection minus the active item (related items).
225
+ const isRouteQuery = dynamicContext && schema === routeSchema
226
+ if (isRouteQuery && !inheritDetail) {
227
+ // refine detail:false — the records minus the active one (related items).
225
228
  const cached = dispatcher?.peek(cfg, ctx)
226
229
  if (cached) {
227
230
  const { paramName, paramValue } = dynamicContext
@@ -234,10 +237,10 @@ export default class EntityStore {
234
237
  } else {
235
238
  allCached = false
236
239
  }
237
- } else if (isRouteCollection) {
240
+ } else if (isRouteQuery) {
238
241
  // Detail page: deliver the focused record as a length-1 array under the
239
- // collection key. Deferred/API collections fetch the full per-record;
240
- // others use the matched item from the collection. Not found → [].
242
+ // query key. A deferred/remote query fetches the full per-record;
243
+ // others use the matched record. Not found → [].
241
244
  const cached = dispatcher?.peek(cfg, ctx)
242
245
  if (cached) {
243
246
  const { paramName, paramValue } = dynamicContext
@@ -283,7 +286,7 @@ export default class EntityStore {
283
286
 
284
287
  /**
285
288
  * Async fetch — dispatches missing configs through the FetcherDispatcher
286
- * and assembles the result. Collection-first detail ordering preserved.
289
+ * and assembles the result. List-first detail ordering preserved.
287
290
  *
288
291
  * @param {Object} [options]
289
292
  * @param {AbortSignal} [options.signal] - Forwarded to the dispatcher.
@@ -316,31 +319,31 @@ export default class EntityStore {
316
319
  const routeSchema = dynamicContext?.schema
317
320
 
318
321
  for (const [schema, cfg] of configs) {
319
- const isRouteCollection = dynamicContext && schema === routeSchema
320
- if (isRouteCollection && !inheritDetail) {
321
- // refine detail:false — the collection minus the active item.
322
- let collectionItems = peekArray(dispatcher, cfg, ctx)
323
- if (collectionItems === null) {
322
+ const isRouteQuery = dynamicContext && schema === routeSchema
323
+ if (isRouteQuery && !inheritDetail) {
324
+ // refine detail:false — the records minus the active one.
325
+ let records = peekArray(dispatcher, cfg, ctx)
326
+ if (records === null) {
324
327
  const result = await dispatcher.dispatch(cfg, ctx)
325
- collectionItems = Array.isArray(result?.data) ? result.data : null
328
+ records = Array.isArray(result?.data) ? result.data : null
326
329
  }
327
330
  const { paramName, paramValue } = dynamicContext
328
- let filtered = Array.isArray(collectionItems)
329
- ? collectionItems.filter((item) => String(item[paramName]) !== String(paramValue))
330
- : (collectionItems ?? [])
331
+ let filtered = Array.isArray(records)
332
+ ? records.filter((item) => String(item[paramName]) !== String(paramValue))
333
+ : (records ?? [])
331
334
  if (order) filtered = this._sortItems(filtered, order)
332
335
  data[schema] = limit && Array.isArray(filtered) ? filtered.slice(0, limit) : filtered
333
- } else if (isRouteCollection) {
334
- // Detail page: focused record as a length-1 array under the collection key.
336
+ } else if (isRouteQuery) {
337
+ // Detail page: focused record as a length-1 array under the query key.
335
338
  const { paramName, paramValue } = dynamicContext
336
339
 
337
- let collectionItems = peekArray(dispatcher, cfg, ctx)
338
- if (collectionItems === null) {
340
+ let records = peekArray(dispatcher, cfg, ctx)
341
+ if (records === null) {
339
342
  const result = await dispatcher.dispatch(cfg, ctx)
340
- collectionItems = Array.isArray(result?.data) ? result.data : null
343
+ records = Array.isArray(result?.data) ? result.data : null
341
344
  }
342
345
 
343
- const match = collectionItems?.find(
346
+ const match = records?.find(
344
347
  (item) => String(item[paramName]) === String(paramValue)
345
348
  ) ?? null
346
349
 
@@ -396,9 +399,9 @@ function peekArray(dispatcher, cfg, ctx) {
396
399
  * Interpolate a record's fields into a detail-page route template to build its
397
400
  * `route` (the canonical href for a card). `/blog/:slug` + `{ slug: 'a-post' }`
398
401
  * → `/blog/a-post`. Returns a SHALLOW COPY with `route` added — never mutates the
399
- * cached record (the same collection may back several sections with different
402
+ * cached record (the same query may back several sections with different
400
403
  * detail pages). Idempotent + back-compat: a record that already carries a `route`
401
- * (the file lane bakes one via collection-processor) is returned untouched. A
404
+ * (the file lane bakes one via the query processor) is returned untouched. A
402
405
  * `:param` with no matching record field → no `route` (graceful; degrades to the
403
406
  * component's own fallback rather than emitting a broken href).
404
407
  */
@@ -4,7 +4,7 @@
4
4
  * "Which fetch configs apply here?" is a framework concept. Authors declare
5
5
  * `fetch:` at the section, page, folder and site levels; the framework decides
6
6
  * which declaration wins per schema, how a local data path is localized, and
7
- * when a collection with deferred fields gets a detail pattern injected.
7
+ * when a query with deferred fields gets a detail pattern injected.
8
8
  *
9
9
  * Every host that renders a page needs that answer — the browser runtime, the
10
10
  * build-time prerenderer, and any server-side renderer. The rule had grown
@@ -27,8 +27,8 @@
27
27
  * `resolveFetchConfigs`. That difference is real and stays with the caller.
28
28
  */
29
29
 
30
- import { collectionDataUrl, isDataUrl, recordDataUrl } from './data-paths.js'
31
- import { resolveCollectionAddress, resolveRecordAddressPattern } from './collection-address.js'
30
+ import { queryDataUrl, isDataUrl, recordDataUrl } from './data-paths.js'
31
+ import { resolveQueryAddress, resolveRecordAddressPattern } from './query-address.js'
32
32
 
33
33
  /**
34
34
  * Is this fetch declaration a per-instance *refinement* of an ancestor's
@@ -66,14 +66,14 @@ function localizeConfig(cfg, locale, defaultLocale) {
66
66
  }
67
67
 
68
68
  /**
69
- * Auto-inject `detail:` on a collection ref whose collection declares
69
+ * Auto-inject `detail:` on a query ref whose query declares
70
70
  * `deferred:` fields.
71
71
  *
72
- * A deferred collection ships a lean list payload, so the full record has to
73
- * come from somewhere else. Two patterns, picked by what the collection
72
+ * A deferred query ships a lean list payload, so the full record has to
73
+ * come from somewhere else. Two patterns, picked by what the query
74
74
  * declares:
75
75
  *
76
- * - the collection has `detailUrl:` → use it verbatim (a remote source);
76
+ * - the query has `detailUrl:` → use it verbatim (a remote source);
77
77
  * - otherwise → `/data/<schema>/{slug}.json`, the per-record file emitted
78
78
  * alongside the lean list.
79
79
  *
@@ -83,39 +83,39 @@ function localizeConfig(cfg, locale, defaultLocale) {
83
83
  * route's paramName is `slug` (the documented convention); a route using
84
84
  * another param name needs an explicit author-written `detail:`.
85
85
  * - Per-record files are not currently localized. A site needing localized
86
- * deferred collections writes its own `detail:` URL.
86
+ * a deferred query writes its own `detail:` URL.
87
87
  *
88
88
  * An author-supplied `cfg.detail` always wins; this only fills the default.
89
- * With no `collections` map available the config passes through untouched —
89
+ * With no `queries` map available the config passes through untouched —
90
90
  * deferred-detail injection is an enhancement, never a correctness
91
- * requirement, so a caller that does not have collection metadata still gets
91
+ * requirement, so a caller that does not have query metadata still gets
92
92
  * a usable config. That matters for hosts whose content projection may not
93
- * carry collection metadata at all.
93
+ * carry query metadata at all.
94
94
  *
95
95
  * @param {Object} cfg
96
- * @param {Object|null} collections - the site's `config.collections` map
96
+ * @param {Object|null} queries - the site's `config.queries` map
97
97
  * @returns {Object} the original config, or a copy carrying `detail`
98
98
  */
99
- function applyDeferredDetail(cfg, collections, records) {
99
+ function applyDeferredDetail(cfg, queries, records) {
100
100
  if (cfg.detail !== undefined) return cfg
101
101
 
102
102
  // ⭐ A lane's record address is injected whenever the lane declares one —
103
- // NOT only for a `deferred:` collection, and the difference is load-bearing.
103
+ // NOT only for a `deferred:` query, and the difference is load-bearing.
104
104
  //
105
105
  // A live lane answers a list request at brief depth and a record request in
106
106
  // full, so a detail page that filtered the list would render the brief and
107
107
  // silently miss the body. And it cannot fall back to the rule below: the
108
- // `deferred:` declaration lives in `config.collections`, which a host's
108
+ // `deferred:` declaration lives in `config.queries`, which a host's
109
109
  // projection is not obliged to carry — so on such a host that rule can never
110
110
  // fire, and this is the only way a detail page reaches a whole record.
111
111
  if (cfg.endpoint) {
112
- const recordPattern = resolveRecordAddressPattern(cfg.collection ?? cfg.schema, records)
112
+ const recordPattern = resolveRecordAddressPattern(cfg.query ?? cfg.schema, records)
113
113
  if (recordPattern) return { ...cfg, detail: recordPattern }
114
114
  }
115
115
 
116
116
  const schema = cfg.schema
117
- if (!schema || !collections) return cfg
118
- const collConfig = collections[schema]
117
+ if (!schema || !queries) return cfg
118
+ const collConfig = queries[schema]
119
119
  if (!collConfig || typeof collConfig !== 'object') return cfg
120
120
  const deferred = Array.isArray(collConfig.deferred) ? collConfig.deferred : null
121
121
  if (!deferred || deferred.length === 0) return cfg
@@ -126,10 +126,15 @@ function applyDeferredDetail(cfg, collections, records) {
126
126
  }
127
127
 
128
128
  /**
129
- * Resolve a `collection:` reference to something the fetcher can call.
129
+ * Resolve a query reference to something the fetcher can call.
130
130
  *
131
- * The author names a collection; this decides where that collection lives, and
132
- * there are exactly two answers:
131
+ * ONE NAME END TO END. The author writes `query:` in queries.yml, the wire
132
+ * carries `query`, and this reads `query`. It said `collection` on the wire for
133
+ * a while, on the belief that the field was the backend's to name — measured
134
+ * otherwise: `fetch` is a blob they carry, not one they model.
135
+ *
136
+ * The author names a query; this decides where its records live, and there are
137
+ * exactly two answers:
133
138
  *
134
139
  * - a host declared a live lane (`config.records`) → an `endpoint`, final on
135
140
  * arrival, which the fetcher calls without composing anything further;
@@ -139,24 +144,24 @@ function applyDeferredDetail(cfg, collections, records) {
139
144
  * every site with no backend, which is the framework's default rather than a
140
145
  * degraded mode — so an absent lane is silent, not warned.
141
146
  *
142
- * ⭐ `collection` OUTRANKS a `path` sitting beside it, which matters because the
143
- * sync producer emits both during the transition — `collection` for a consumer
144
- * that resolves it, `path` for one that has not been taught to yet. Resolving
145
- * whenever `collection` is present is also what the build-time parser has always
146
- * done (`parseFetchConfig` returns early on `collection`, ignoring any `path`),
147
- * so the two agree rather than disagreeing on a shape nobody hand-writes.
147
+ * ⭐ `query` OUTRANKS a `path` sitting beside it, which matters because the sync
148
+ * producer emits both — `query` for a consumer that resolves it, `path` as the
149
+ * artifact address for one that cannot. Resolving whenever `query` is present is
150
+ * also what the build-time parser does (`parseFetchConfig` returns early on
151
+ * `query`, ignoring any `path`), so the two agree rather than disagreeing on a
152
+ * shape nobody hand-writes.
148
153
  */
149
- function resolveCollectionSource(cfg, records) {
150
- if (typeof cfg.collection !== 'string' || cfg.collection.length === 0) return cfg
154
+ function resolveQuerySource(cfg, records) {
155
+ if (typeof cfg.query !== 'string' || cfg.query.length === 0) return cfg
151
156
 
152
- const endpoint = resolveCollectionAddress(cfg.collection, records)
157
+ const endpoint = resolveQueryAddress(cfg.query, records)
153
158
  if (endpoint) {
154
159
  // Drop the transitional `path`: two addresses on one request is an
155
160
  // ambiguity the fetcher would have to break by accident of field order.
156
161
  const { path, url, ...rest } = cfg
157
162
  return { ...rest, endpoint }
158
163
  }
159
- return { ...cfg, path: collectionDataUrl(cfg.collection) }
164
+ return { ...cfg, path: queryDataUrl(cfg.query) }
160
165
  }
161
166
 
162
167
  /**
@@ -179,9 +184,9 @@ function resolveCollectionSource(cfg, records) {
179
184
  * Empty (the default) collects every schema found.
180
185
  * @param {string|null} [options.locale] - the locale being rendered
181
186
  * @param {string|null} [options.defaultLocale] - the site's default locale
182
- * @param {Object|null} [options.collections] - the site's `config.collections`
187
+ * @param {Object|null} [options.queries] - the site's `config.queries`
183
188
  * @param {Object|null} [options.records] - the site's `config.records`, a host's
184
- * live-collection lane. Absent means the compiled artifact answers, which is
189
+ * live-records lane. Absent means the compiled artifact answers, which is
185
190
  * the whole of what a site with no backend needs.
186
191
  * @returns {Map<string, Object>} schema name → resolved config
187
192
  */
@@ -190,7 +195,7 @@ export function resolveFetchConfigs(sources, options = {}) {
190
195
  schemas = [],
191
196
  locale = null,
192
197
  defaultLocale = null,
193
- collections = null,
198
+ queries = null,
194
199
  records = null,
195
200
  } = options
196
201
 
@@ -205,10 +210,10 @@ export function resolveFetchConfigs(sources, options = {}) {
205
210
  if (configs.has(cfg.schema)) continue
206
211
  if (!collectAll && !schemas.includes(cfg.schema)) continue
207
212
  // Address first: localization and deferred-detail both key on `path`,
208
- // which a `collection:` ref does not have until this runs.
209
- const sourced = resolveCollectionSource(cfg, records)
213
+ // which a query ref does not have until this runs.
214
+ const sourced = resolveQuerySource(cfg, records)
210
215
  const localized = localizeConfig(sourced, locale, defaultLocale)
211
- configs.set(cfg.schema, applyDeferredDetail(localized, collections, records))
216
+ configs.set(cfg.schema, applyDeferredDetail(localized, queries, records))
212
217
  }
213
218
  }
214
219
 
@@ -122,7 +122,7 @@ export default class FetcherDispatcher {
122
122
  })
123
123
  } catch (err) {
124
124
  if (this._dev) {
125
- console.warn('[FetcherDispatcher] extension transports collection threw:', err)
125
+ console.warn('[FetcherDispatcher] extension transports list threw:', err)
126
126
  }
127
127
  continue
128
128
  }
package/src/index.js CHANGED
@@ -21,9 +21,9 @@ export { default as ObservableState } from './observable-state.js'
21
21
  // Utilities
22
22
  export { substitutePlaceholders } from './substitute-placeholders.js'
23
23
  export {
24
- resolveCollectionAddress,
24
+ resolveQueryAddress,
25
25
  resolveRecordAddressPattern,
26
- } from './collection-address.js'
26
+ } from './query-address.js'
27
27
  export { resolveFetchConfigs } from './fetch-config.js'
28
28
  export { buildDetailConfig } from './detail-url.js'
29
29
  export {
@@ -36,9 +36,9 @@ export {
36
36
  export {
37
37
  DATA_DIR,
38
38
  DATA_URL_PREFIX,
39
- collectionDataUrl,
39
+ queryDataUrl,
40
40
  recordDataUrl,
41
- collectionNameFromUrl,
41
+ queryNameFromUrl,
42
42
  isDataUrl
43
43
  } from './data-paths.js'
44
44
  export { evaluate as evaluateWhere, match as matchWhere } from './where.js'
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Resolve a collection request to an address the fetcher can call.
2
+ * Resolve a query request to an address the fetcher can call.
3
3
  *
4
4
  * ## The one idea
5
5
  *
6
- * A site names a collection; it never names where the collection lives. Where
6
+ * A site names a query; it never names where its records live. Where
7
7
  * it lives is a **deployment** fact, and the two possible answers have different
8
8
  * owners:
9
9
  *
@@ -24,7 +24,7 @@
24
24
  *
25
25
  * A base assumes the layout is "root plus one segment". A pattern assumes
26
26
  * nothing, so a host can carry a site id, a locale segment, a different root
27
- * for records than for the collection, or none of those, and move any of it
27
+ * for records than for the list, or none of those, and move any of it
28
28
  * without a framework release.
29
29
  *
30
30
  * This is the `config.assets.url` rule applied to records. That pattern exists
@@ -45,7 +45,7 @@ import { substitutePlaceholders } from './substitute-placeholders.js'
45
45
  /**
46
46
  * The placeholder a list pattern must carry.
47
47
  *
48
- * ⛔ IT IS `{path}`, NOT `{collection}`, AND THAT IS NOT COSMETIC. A *collection* is
48
+ * ⛔ IT IS `{path}`, NOT `{query}`, AND THAT IS NOT COSMETIC. A *query* is
49
49
  * framework's own build concept — a named set our build compiles to one file. A host
50
50
  * serving records has no such thing: it has content organised somewhere, and what we
51
51
  * substitute is a **path** to it. Naming the slot for our file vocabulary put that
@@ -61,11 +61,11 @@ const warnedPatterns = new Set()
61
61
  function warnOnce(key, message) {
62
62
  if (warnedPatterns.has(key)) return
63
63
  warnedPatterns.add(key)
64
- console.warn(`[collection-address] ${message}`)
64
+ console.warn(`[query-address] ${message}`)
65
65
  }
66
66
 
67
67
  /** Test seam — reset the once-per-pattern memo so suites do not leak. */
68
- export function _resetCollectionAddressWarnings() {
68
+ export function _resetQueryAddressWarnings() {
69
69
  warnedPatterns.clear()
70
70
  }
71
71
 
@@ -83,36 +83,36 @@ function readPattern(lane, key) {
83
83
  }
84
84
 
85
85
  /**
86
- * The address for a whole collection, or `null` to fall through to the artifact.
86
+ * The address for a whole query's records, or `null` to fall through to the artifact.
87
87
  *
88
88
  * ⚠️ A pattern that does not carry `{path}` is REFUSED rather than used.
89
- * Substituting nothing would yield one identical URL for every collection on the
89
+ * Substituting nothing would yield one identical URL for every query on the
90
90
  * site — every schema reading the same records, with a 200 on each request. That
91
91
  * is the failure this check exists for; an unusable pattern must degrade to the
92
92
  * artifact, which is at least correct.
93
93
  *
94
- * @param {string} collection - the collection's authored name (the wiring key).
94
+ * @param {string} query - the query's authored name (the wiring key).
95
95
  * @param {Object|null} lane - `config.records`.
96
96
  * @returns {string|null} the address, or null when nothing usable is declared.
97
97
  */
98
- export function resolveCollectionAddress(collection, lane) {
99
- if (typeof collection !== 'string' || collection.length === 0) return null
98
+ export function resolveQueryAddress(query, lane) {
99
+ if (typeof query !== 'string' || query.length === 0) return null
100
100
  const pattern = readPattern(lane, 'list')
101
101
  if (!pattern) return null
102
102
  if (!pattern.includes(PATH_SLOT)) {
103
103
  warnOnce(
104
104
  `list:${pattern}`,
105
105
  `config.records.list carries no ${PATH_SLOT} placeholder, so every ` +
106
- `collection would resolve to the same address. Ignoring it and reading the ` +
107
- `compiled collection file instead.`
106
+ `query would resolve to the same address. Ignoring it and reading the ` +
107
+ `compiled file instead.`
108
108
  )
109
109
  return null
110
110
  }
111
- return substitutePlaceholders(pattern, { path: collection })
111
+ return substitutePlaceholders(pattern, { path: query })
112
112
  }
113
113
 
114
114
  /**
115
- * The address pattern for ONE record of a collection, with `{param}` left in
115
+ * The address pattern for ONE record of a query, with `{param}` left in
116
116
  * place for the dynamic-route substitution that happens later.
117
117
  *
118
118
  * Returning a pattern rather than a finished URL is deliberate: the route param
@@ -120,12 +120,12 @@ export function resolveCollectionAddress(collection, lane) {
120
120
  * (`buildDetailConfig` / `substitutePlaceholders` at fetch time). Resolving it
121
121
  * twice, in two places, is how the two copies drift.
122
122
  *
123
- * @param {string} collection
123
+ * @param {string} query
124
124
  * @param {Object|null} lane - `config.records`.
125
125
  * @returns {string|null} a pattern still containing `{param}`, or null.
126
126
  */
127
- export function resolveRecordAddressPattern(collection, lane) {
128
- if (typeof collection !== 'string' || collection.length === 0) return null
127
+ export function resolveRecordAddressPattern(query, lane) {
128
+ if (typeof query !== 'string' || query.length === 0) return null
129
129
  const pattern = readPattern(lane, 'record')
130
130
  if (!pattern) return null
131
131
  if (!pattern.includes(PARAM_SLOT)) {
@@ -139,5 +139,5 @@ export function resolveRecordAddressPattern(collection, lane) {
139
139
  }
140
140
  // Only `{path}` is substituted here — `{param}` survives for the
141
141
  // dynamic-route resolution that owns it.
142
- return substitutePlaceholders(pattern, { path: collection })
142
+ return substitutePlaceholders(pattern, { path: query })
143
143
  }
@@ -28,7 +28,7 @@
28
28
  * sort: 'date desc, title asc'
29
29
  * → sort[0]=date:desc & sort[1]=title:asc
30
30
  *
31
- * Response envelope defaults to `{ collection: 'data', item: 'data' }` —
31
+ * Response envelope defaults to `{ list: 'data', item: 'data' }` —
32
32
  * Strapi v4 wraps every response in `{ data, meta }`. Sites can override
33
33
  * via `envelope:` at the site or per-fetch level.
34
34
  *
@@ -61,7 +61,7 @@
61
61
  export const strapi = {
62
62
  name: 'strapi',
63
63
  canPush: new Set(['where', 'limit', 'sort']),
64
- defaultEnvelope: { collection: 'data', item: 'data' },
64
+ defaultEnvelope: { list: 'data', item: 'data' },
65
65
 
66
66
  encode(request, { method, pushCandidates, rename }) {
67
67
  const pushed = new Set()
package/src/website.js CHANGED
@@ -506,11 +506,11 @@ export default class Website {
506
506
  // Create a dynamic page instance with the concrete route and params
507
507
  const result = this._createDynamicPage(page, normalizedRoute, match.params)
508
508
  if (result) {
509
- const { page: dynamicPage, collectionLoaded } = result
510
- // Only cache when collection data was available at creation time.
509
+ const { page: dynamicPage, recordsLoaded } = result
510
+ // Only cache when the records were available at creation time.
511
511
  // If DataStore was empty, skip caching so the next render recreates
512
512
  // the page with fresh data (correct title, not-found state, etc.).
513
- if (collectionLoaded) {
513
+ if (recordsLoaded) {
514
514
  this._dynamicPageCache.set(normalizedRoute, dynamicPage)
515
515
  }
516
516
  return dynamicPage
@@ -584,7 +584,7 @@ export default class Website {
584
584
  const parentPage = this.pages.find(p => p.route === parentRoute || p.getNavRoute() === parentRoute)
585
585
 
586
586
  if (parentPage && pluralSchema) {
587
- // Find collection data from parent's fetch config via the dispatcher's
587
+ // Find the records from the parent's fetch config via the dispatcher's
588
588
  // peek (sync cache probe). Used to populate the page title / notFound
589
589
  // flag on dynamic pages before the page instance is constructed.
590
590
  const parentFetch = parentPage.fetch
@@ -608,18 +608,18 @@ export default class Website {
608
608
  pageData.description = currentItem.description || currentItem.excerpt
609
609
  }
610
610
  } else if (items.length > 0) {
611
- // Collection is loaded but this ID isn't in it — definitive not found
611
+ // The records are loaded but this ID isn't among them — definitive not found
612
612
  pageData.title = 'Not found'
613
613
  pageData.notFound = true
614
614
  }
615
615
 
616
- // Track whether collection data was available at creation time.
616
+ // Track whether the records were available at creation time.
617
617
  // Note: the matched record and the sibling list are intentionally NOT
618
618
  // stored on dynamicContext — nothing reads them (documented shape is
619
619
  // { paramName, paramValue, schema }; the record reaches components via
620
620
  // content.data, siblings via `fetch: { refine: true, detail: false }`).
621
621
  // The local `currentItem`/`items` above drive title/description/notFound.
622
- pageData._collectionLoaded = items.length > 0
622
+ pageData._recordsLoaded = items.length > 0
623
623
  }
624
624
 
625
625
  // Create the page instance
@@ -628,7 +628,7 @@ export default class Website {
628
628
  // Copy parent reference from template
629
629
  dynamicPage.parent = templatePage.parent
630
630
 
631
- return { page: dynamicPage, collectionLoaded: pageData._collectionLoaded ?? true }
631
+ return { page: dynamicPage, recordsLoaded: pageData._recordsLoaded ?? true }
632
632
  }
633
633
 
634
634
  /**
@@ -813,7 +813,7 @@ export default class Website {
813
813
  * Resolve a `page:<stable_id>` detail-page reference (from a fetch config's
814
814
  * `detailPage`) to a locale-specific route TEMPLATE, e.g. '/blog/:slug'. The
815
815
  * entity store interpolates each record's field into the `:param` slot to build
816
- * a card's href — so a dynamic-list preview links to the collection's canonical
816
+ * a card's href — so a dynamic-list preview links to the query's canonical
817
817
  * detail page regardless of which page it sits on.
818
818
  *
819
819
  * O(1): a `_pageIdMap` lookup (keyed on stable_id, same map makeHref uses), NOT
package/src/where.js CHANGED
@@ -45,7 +45,7 @@
45
45
  * not Single sub-predicate; must not match.
46
46
  *
47
47
  * `under` is for fields holding a slash-separated location — a record's
48
- * position inside its collection, a category path, a docs section. It matches
48
+ * position inside the folder, a category path, a docs section. It matches
49
49
  * the value itself and anything below it, and it respects segment boundaries so
50
50
  * a sibling with a shared prefix does not match:
51
51
  *
@@ -143,7 +143,7 @@ function evaluateClause(key, value, record) {
143
143
  *
144
144
  * Both sides are compared with leading and trailing `/` trimmed, so an author
145
145
  * who writes `'/2024'` or `'2024/'` gets what they meant. An empty ancestor is
146
- * the root and contains everything — which is what makes "the whole collection"
146
+ * the root and contains everything — which is what makes "the whole pool"
147
147
  * expressible as a predicate rather than as the absence of one.
148
148
  *
149
149
  * ⛔ The `+ '/'` is the whole point: a plain `startsWith` would match `2024b`