@uniweb/core 0.16.0 → 0.18.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 +4 -3
- package/src/datastore.js +11 -2
- package/src/entity-store.js +9 -2
- package/src/fetch-config.js +50 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"./collection-address": "./src/query-address.js",
|
|
10
10
|
"./query-address": "./src/query-address.js",
|
|
11
11
|
"./data-paths": "./src/data-paths.js",
|
|
12
|
+
"./datastore": "./src/datastore.js",
|
|
12
13
|
"./detail-url": "./src/detail-url.js",
|
|
13
14
|
"./fetch-config": "./src/fetch-config.js",
|
|
14
15
|
"./icon-corpus": "./src/icon-corpus.js",
|
|
@@ -42,8 +43,8 @@
|
|
|
42
43
|
"vitest": "^4.1.7"
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
46
|
+
"@uniweb/semantic-parser": "^1.4.0",
|
|
47
|
+
"@uniweb/theming": "^0.1.15"
|
|
47
48
|
},
|
|
48
49
|
"scripts": {
|
|
49
50
|
"test": "vitest run"
|
package/src/datastore.js
CHANGED
|
@@ -35,12 +35,21 @@
|
|
|
35
35
|
* @returns {string} A stable JSON string usable as a cache-Map key
|
|
36
36
|
*/
|
|
37
37
|
export function deriveCacheKey(request) {
|
|
38
|
-
|
|
38
|
+
// ⭐ `as` is the binding key; `schema` is the name it had until 2026-09-02 and
|
|
39
|
+
// still arrives on every payload published before then. Normalised HERE so one
|
|
40
|
+
// logical request hashes to one key whichever spelling reached us — a consumer
|
|
41
|
+
// deriving a key must not get two entries for the same fetch.
|
|
42
|
+
const { path, url, endpoint, transform } = request || {}
|
|
43
|
+
const as = request?.as ?? request?.schema
|
|
39
44
|
const method = request?.method && request.method.toUpperCase() !== 'GET'
|
|
40
45
|
? request.method.toUpperCase()
|
|
41
46
|
: undefined
|
|
42
47
|
const body = method === 'POST' ? request?.body : undefined
|
|
43
|
-
|
|
48
|
+
// ⚠️ The field NAME is part of the hash, so renaming it moves every key ONCE.
|
|
49
|
+
// In-memory stores repopulate; a consumer with a persistent cache takes one
|
|
50
|
+
// cold pass. Chosen over hashing under the old name, which would have hidden
|
|
51
|
+
// the rename inside the one function whose job is to be canonical.
|
|
52
|
+
return JSON.stringify({ path, url, endpoint, as, transform, method, body })
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
export default class DataStore {
|
package/src/entity-store.js
CHANGED
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { isFetchRefinement, resolveFetchConfigs } from './fetch-config.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A fetch config's binding key — the `content.data.<key>` a component reads.
|
|
21
|
+
* `as` is the name; `schema` is what it was called until 2026-09-02 and still
|
|
22
|
+
* arrives on any payload published before then. See `fetch-config.js`.
|
|
23
|
+
*/
|
|
24
|
+
const bindingKeyOf = (cfg) => cfg?.as ?? cfg?.schema
|
|
18
25
|
import { buildDetailConfig } from './detail-url.js'
|
|
19
26
|
|
|
20
27
|
/**
|
|
@@ -201,7 +208,7 @@ export default class EntityStore {
|
|
|
201
208
|
// collecting all cascade matches.
|
|
202
209
|
if (requested === null && block.fetch) {
|
|
203
210
|
const blockFetchList = Array.isArray(block.fetch) ? block.fetch : [block.fetch]
|
|
204
|
-
const schemas = blockFetchList.filter(
|
|
211
|
+
const schemas = blockFetchList.filter(bindingKeyOf).map(bindingKeyOf)
|
|
205
212
|
if (schemas.length > 0) requested = schemas
|
|
206
213
|
}
|
|
207
214
|
|
|
@@ -299,7 +306,7 @@ export default class EntityStore {
|
|
|
299
306
|
let requested = this._getRequestedSchemas(meta)
|
|
300
307
|
if (requested === null && block.fetch) {
|
|
301
308
|
const blockFetchList = Array.isArray(block.fetch) ? block.fetch : [block.fetch]
|
|
302
|
-
const schemas = blockFetchList.filter(
|
|
309
|
+
const schemas = blockFetchList.filter(bindingKeyOf).map(bindingKeyOf)
|
|
303
310
|
if (schemas.length > 0) requested = schemas
|
|
304
311
|
}
|
|
305
312
|
if (requested === null) return { data: null }
|
package/src/fetch-config.js
CHANGED
|
@@ -109,19 +109,37 @@ function applyDeferredDetail(cfg, queries, records) {
|
|
|
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
|
-
|
|
112
|
+
// ⛔ `cfg.query`, not `cfg.query ?? cfg.schema`. The `??` was unreachable:
|
|
113
|
+
// `endpoint` is set in exactly one place (`resolveQuerySource`), which returns
|
|
114
|
+
// early unless `cfg.query` is a non-empty string — so reaching here proves it.
|
|
115
|
+
// It read as a tolerance for two producer shapes and was really a vestige of
|
|
116
|
+
// the build lane not emitting `query`, which it now does.
|
|
117
|
+
const recordPattern = resolveRecordAddressPattern(cfg.query, records)
|
|
113
118
|
if (recordPattern) return { ...cfg, detail: recordPattern }
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
// ⛔ **`config.queries` is keyed by QUERY NAME, so look it up by the query.**
|
|
122
|
+
// This read `cfg.schema` — the BINDING KEY, which merely defaults to the query
|
|
123
|
+
// name. `fetch: { query: 'articles', schema: 'posts' }` is a supported, allow-
|
|
124
|
+
// listed, unwarned form (`RECOGNIZED_FETCH_KEYS.query`), and under it the lookup
|
|
125
|
+
// missed and a detail page silently rendered the brief without its body.
|
|
126
|
+
// Measured 2026-09-01, control passing: `{query:'articles'}` resolved
|
|
127
|
+
// `/data/articles/{slug}.json`; `{query:'articles',schema:'posts'}` resolved
|
|
128
|
+
// nothing, from the same file.
|
|
129
|
+
//
|
|
130
|
+
// ⚖️ The `|| cfg.schema` is NOT the vestige deleted above. A source-shape fetch
|
|
131
|
+
// (`{ path: … }`) has no query at all, and its schema — inferred from the path —
|
|
132
|
+
// is the only key there is. Two shapes, two answers; the deleted one had one
|
|
133
|
+
// shape and pretended otherwise.
|
|
134
|
+
const queryName = cfg.query || bindingKey(cfg)
|
|
135
|
+
if (!queryName || !queries) return cfg
|
|
136
|
+
const collConfig = queries[queryName]
|
|
119
137
|
if (!collConfig || typeof collConfig !== 'object') return cfg
|
|
120
138
|
const deferred = Array.isArray(collConfig.deferred) ? collConfig.deferred : null
|
|
121
139
|
if (!deferred || deferred.length === 0) return cfg
|
|
122
140
|
const pattern = typeof collConfig.detailUrl === 'string'
|
|
123
141
|
? collConfig.detailUrl
|
|
124
|
-
: recordDataUrl(
|
|
142
|
+
: recordDataUrl(queryName, '{slug}')
|
|
125
143
|
return { ...cfg, detail: pattern }
|
|
126
144
|
}
|
|
127
145
|
|
|
@@ -164,6 +182,28 @@ function resolveQuerySource(cfg, records) {
|
|
|
164
182
|
return { ...cfg, path: queryDataUrl(cfg.query) }
|
|
165
183
|
}
|
|
166
184
|
|
|
185
|
+
/**
|
|
186
|
+
* The binding key of a fetch config — the `content.data.<key>` a component reads.
|
|
187
|
+
*
|
|
188
|
+
* ⭐ **`as` is the name; `schema` is what it was called until 2026-09-02.** The old
|
|
189
|
+
* spelling still arrives on every payload published before then and on any seed
|
|
190
|
+
* built against an older release, so this is not a deprecation window — it is a
|
|
191
|
+
* permanent reader of stored data. *(Renaming it was not cosmetic: `schema`
|
|
192
|
+
* already means the MODEL REF one record over, on a `queries` declaration, and
|
|
193
|
+
* one word for two things is what let a binding-key override silently break
|
|
194
|
+
* detail resolution.)*
|
|
195
|
+
*
|
|
196
|
+
* ⛔ Do not "simplify" this to `cfg.as`. The `??` here is earned — it spans
|
|
197
|
+
* stored payloads we cannot rewrite — unlike the one deleted from
|
|
198
|
+
* `applyDeferredDetail`, which spanned two producers we control.
|
|
199
|
+
*
|
|
200
|
+
* @param {Object} cfg
|
|
201
|
+
* @returns {string|undefined}
|
|
202
|
+
*/
|
|
203
|
+
function bindingKey(cfg) {
|
|
204
|
+
return cfg?.as ?? cfg?.schema
|
|
205
|
+
}
|
|
206
|
+
|
|
167
207
|
/**
|
|
168
208
|
* Resolve the applicable fetch configs from an ordered list of sources.
|
|
169
209
|
*
|
|
@@ -206,14 +246,15 @@ export function resolveFetchConfigs(sources, options = {}) {
|
|
|
206
246
|
if (!source) continue
|
|
207
247
|
const configList = Array.isArray(source) ? source : [source]
|
|
208
248
|
for (const cfg of configList) {
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
if (
|
|
249
|
+
const key = bindingKey(cfg)
|
|
250
|
+
if (!key) continue
|
|
251
|
+
if (configs.has(key)) continue
|
|
252
|
+
if (!collectAll && !schemas.includes(key)) continue
|
|
212
253
|
// Address first: localization and deferred-detail both key on `path`,
|
|
213
254
|
// which a query ref does not have until this runs.
|
|
214
255
|
const sourced = resolveQuerySource(cfg, records)
|
|
215
256
|
const localized = localizeConfig(sourced, locale, defaultLocale)
|
|
216
|
-
configs.set(
|
|
257
|
+
configs.set(key, applyDeferredDetail(localized, queries, records))
|
|
217
258
|
}
|
|
218
259
|
}
|
|
219
260
|
|