@jini-ai/cms 0.5.2 → 0.5.3
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/CHANGELOG.md +11 -0
- package/README.md +14 -8
- package/dist/comments/errors.d.ts +23 -0
- package/dist/comments/errors.js +25 -0
- package/dist/comments/express/index.d.ts +2 -0
- package/dist/comments/express/index.js +2 -0
- package/dist/comments/express/submit-handler.d.ts +30 -0
- package/dist/comments/express/submit-handler.js +85 -0
- package/dist/comments/hooks.d.ts +39 -0
- package/dist/comments/hooks.js +41 -0
- package/dist/comments/index.d.ts +11 -0
- package/dist/comments/index.js +11 -0
- package/dist/comments/ingress.d.ts +57 -0
- package/dist/comments/ingress.js +173 -0
- package/dist/comments/module.d.ts +94 -0
- package/dist/comments/module.js +73 -0
- package/dist/comments/ports.d.ts +184 -0
- package/dist/comments/ports.js +2 -0
- package/dist/comments/repo.memory.d.ts +70 -0
- package/dist/comments/repo.memory.js +120 -0
- package/dist/comments/sanitize.d.ts +15 -0
- package/dist/comments/sanitize.js +31 -0
- package/dist/comments/spam.heuristic.d.ts +8 -0
- package/dist/comments/spam.heuristic.js +54 -0
- package/dist/comments/sql/index.d.ts +3 -0
- package/dist/comments/sql/index.js +3 -0
- package/dist/comments/sql/repo.d.ts +77 -0
- package/dist/comments/sql/repo.js +190 -0
- package/dist/comments/sql/repo.rows.d.ts +49 -0
- package/dist/comments/sql/repo.rows.js +71 -0
- package/dist/comments/types.d.ts +187 -0
- package/dist/comments/types.js +39 -0
- package/dist/comments/write-service.d.ts +121 -0
- package/dist/comments/write-service.js +144 -0
- package/dist/core/commands/command.d.ts +2 -9
- package/dist/core/commands/command.js +2 -9
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +1 -0
- package/dist/core/keyset-cursor.d.ts +25 -0
- package/dist/core/keyset-cursor.js +20 -0
- package/dist/core/ports.d.ts +30 -0
- package/dist/core/ports.js +0 -1
- package/dist/core/tools/args.d.ts +1 -46
- package/dist/core/tools/args.js +1 -44
- package/dist/media/index.d.ts +0 -3
- package/dist/media/index.js +1 -4
- package/dist/media/node.d.ts +5 -0
- package/dist/media/node.js +6 -0
- package/dist/redirects/capture.d.ts +80 -0
- package/dist/redirects/capture.js +66 -0
- package/dist/redirects/express/index.d.ts +27 -0
- package/dist/redirects/express/index.js +40 -0
- package/dist/redirects/hit-sink.d.ts +56 -0
- package/dist/redirects/hit-sink.js +44 -0
- package/dist/redirects/host-config/index.d.ts +40 -0
- package/dist/redirects/host-config/index.js +145 -0
- package/dist/redirects/index.d.ts +14 -0
- package/dist/redirects/index.js +14 -0
- package/dist/redirects/matcher.d.ts +58 -0
- package/dist/redirects/matcher.js +144 -0
- package/dist/redirects/order.d.ts +30 -0
- package/dist/redirects/order.js +26 -0
- package/dist/redirects/ports.d.ts +242 -0
- package/dist/redirects/ports.internal.d.ts +57 -0
- package/dist/redirects/ports.internal.js +14 -0
- package/dist/redirects/ports.js +2 -0
- package/dist/redirects/redirects.d.ts +226 -0
- package/dist/redirects/redirects.js +590 -0
- package/dist/redirects/referrer-alias.d.ts +31 -0
- package/dist/redirects/referrer-alias.js +33 -0
- package/dist/redirects/repo.memory.d.ts +51 -0
- package/dist/redirects/repo.memory.js +98 -0
- package/dist/redirects/reserved-destination.d.ts +58 -0
- package/dist/redirects/reserved-destination.js +59 -0
- package/dist/redirects/resolver.d.ts +100 -0
- package/dist/redirects/resolver.js +87 -0
- package/dist/redirects/spa/index.d.ts +30 -0
- package/dist/redirects/spa/index.js +27 -0
- package/dist/redirects/sql/index.d.ts +4 -0
- package/dist/redirects/sql/index.js +4 -0
- package/dist/redirects/sql/repo.d.ts +82 -0
- package/dist/redirects/sql/repo.js +143 -0
- package/dist/redirects/sql/repo.rows.d.ts +49 -0
- package/dist/redirects/sql/repo.rows.js +79 -0
- package/dist/redirects/sql/row-types.d.ts +34 -0
- package/dist/redirects/sql/row-types.js +2 -0
- package/dist/redirects/target-gate.d.ts +30 -0
- package/dist/redirects/target-gate.js +85 -0
- package/dist/redirects/types.d.ts +213 -0
- package/dist/redirects/types.js +42 -0
- package/dist/seo/absolute-url.d.ts +58 -0
- package/dist/seo/absolute-url.js +117 -0
- package/dist/seo/dom/index.d.ts +14 -0
- package/dist/seo/dom/index.js +52 -0
- package/dist/seo/errors.d.ts +35 -0
- package/dist/seo/errors.js +35 -0
- package/dist/seo/feed.d.ts +49 -0
- package/dist/seo/feed.js +61 -0
- package/dist/seo/index.d.ts +13 -0
- package/dist/seo/index.js +13 -0
- package/dist/seo/media.d.ts +57 -0
- package/dist/seo/media.js +134 -0
- package/dist/seo/page-head.d.ts +18 -0
- package/dist/seo/page-head.js +114 -0
- package/dist/seo/ports.d.ts +221 -0
- package/dist/seo/ports.js +27 -0
- package/dist/seo/seo.d.ts +73 -0
- package/dist/seo/seo.js +230 -0
- package/dist/seo/sitemap.d.ts +139 -0
- package/dist/seo/sitemap.js +217 -0
- package/dist/seo/types.d.ts +278 -0
- package/dist/seo/types.js +2 -0
- package/dist/seo/write-service.d.ts +79 -0
- package/dist/seo/write-service.js +273 -0
- package/dist/seo/xml-escape.d.ts +16 -0
- package/dist/seo/xml-escape.js +19 -0
- package/dist/settings/express/cms-adapter.d.ts +20 -0
- package/dist/settings/express/cms-adapter.js +124 -0
- package/dist/settings/express/contracts.d.ts +135 -0
- package/dist/settings/express/contracts.js +2 -0
- package/dist/settings/express/http.d.ts +10 -0
- package/dist/settings/express/http.js +211 -0
- package/dist/settings/express/index.d.ts +5 -0
- package/dist/settings/express/index.js +5 -0
- package/dist/settings/express/shared.d.ts +68 -0
- package/dist/settings/express/shared.js +54 -0
- package/dist/settings/express/stream.d.ts +24 -0
- package/dist/settings/express/stream.js +147 -0
- package/dist/settings/index.d.ts +3 -38
- package/dist/settings/index.js +3 -35
- package/dist/taxonomy/write-service.d.ts +2 -30
- package/dist/widgets/authorize-helper.d.ts +42 -0
- package/dist/widgets/authorize-helper.js +16 -0
- package/dist/widgets/concurrency.d.ts +12 -0
- package/dist/widgets/concurrency.js +44 -0
- package/dist/widgets/config-validation.d.ts +34 -0
- package/dist/widgets/config-validation.js +125 -0
- package/dist/widgets/embed-service.d.ts +133 -0
- package/dist/widgets/embed-service.js +460 -0
- package/dist/widgets/embed-validation.d.ts +23 -0
- package/dist/widgets/embed-validation.js +38 -0
- package/dist/widgets/entry-payload.d.ts +103 -0
- package/dist/widgets/entry-payload.js +160 -0
- package/dist/widgets/errors.d.ts +136 -0
- package/dist/widgets/errors.js +136 -0
- package/dist/widgets/host-ports.d.ts +149 -0
- package/dist/widgets/host-ports.js +2 -0
- package/dist/widgets/html/html-embeds.d.ts +247 -0
- package/dist/widgets/html/html-embeds.js +408 -0
- package/dist/widgets/html/index.d.ts +5 -0
- package/dist/widgets/html/index.js +4 -0
- package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
- package/dist/widgets/html/legacy-widget-markers.js +45 -0
- package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
- package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +19 -0
- package/dist/widgets/markers/html-attributes.d.ts +98 -0
- package/dist/widgets/markers/html-attributes.js +216 -0
- package/dist/widgets/markers/index.d.ts +3 -0
- package/dist/widgets/markers/index.js +3 -0
- package/dist/widgets/markers/marker.d.ts +496 -0
- package/dist/widgets/markers/marker.js +780 -0
- package/dist/widgets/page-embed-types.d.ts +42 -0
- package/dist/widgets/page-embed-types.js +42 -0
- package/dist/widgets/ports.d.ts +142 -0
- package/dist/widgets/ports.js +2 -0
- package/dist/widgets/read-service.d.ts +81 -0
- package/dist/widgets/read-service.js +79 -0
- package/dist/widgets/region-area-service.d.ts +110 -0
- package/dist/widgets/region-area-service.js +180 -0
- package/dist/widgets/registry.d.ts +217 -0
- package/dist/widgets/registry.js +172 -0
- package/dist/widgets/repo.memory.d.ts +41 -0
- package/dist/widgets/repo.memory.js +48 -0
- package/dist/widgets/resolver-service.d.ts +209 -0
- package/dist/widgets/resolver-service.js +909 -0
- package/dist/widgets/resolvers/contact-form.d.ts +26 -0
- package/dist/widgets/resolvers/contact-form.js +54 -0
- package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
- package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
- package/dist/widgets/resolvers/index.d.ts +65 -0
- package/dist/widgets/resolvers/index.js +155 -0
- package/dist/widgets/resolvers/menu.d.ts +42 -0
- package/dist/widgets/resolvers/menu.js +43 -0
- package/dist/widgets/resolvers/ports.d.ts +82 -0
- package/dist/widgets/resolvers/ports.js +2 -0
- package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
- package/dist/widgets/resolvers/recent-entries.js +165 -0
- package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
- package/dist/widgets/resolvers/taxonomy.js +34 -0
- package/dist/widgets/sql/index.d.ts +3 -0
- package/dist/widgets/sql/index.js +3 -0
- package/dist/widgets/sql/repo.d.ts +41 -0
- package/dist/widgets/sql/repo.js +82 -0
- package/dist/widgets/sql/tables.d.ts +13 -0
- package/dist/widgets/sql/tables.js +2 -0
- package/dist/widgets/types.d.ts +213 -0
- package/dist/widgets/types.js +17 -0
- package/dist/widgets/where-used.d.ts +41 -0
- package/dist/widgets/where-used.js +11 -0
- package/dist/widgets/write-service.d.ts +204 -0
- package/dist/widgets/write-service.js +332 -0
- package/dist/workspace/delete.d.ts +1 -1
- package/package.json +220 -11
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { getWidgetTypeRegistration } from "../registry.js";
|
|
2
|
+
const NO_DISPLAY_FIELDS = [];
|
|
3
|
+
/** Prefers `publishedAt`; falls back to `updatedAt` for the rare row with no `publishedAt` yet
|
|
4
|
+
* (draft rows never reach either path — both queries filter to `status: 'published'` — but a
|
|
5
|
+
* defensive fallback costs nothing). @complexity O(1). */
|
|
6
|
+
function entryDateIso(entry) {
|
|
7
|
+
return entry.publishedAt ?? entry.updatedAt;
|
|
8
|
+
}
|
|
9
|
+
/** Pure display formatting for an entry's date (e.g. "Sep 3, 2026") — the same short en-US
|
|
10
|
+
* convention `pages.ts`'s `formatPostPreviewDate` already established for post-previews/collection
|
|
11
|
+
* cards. Duplicated rather than imported: that function is file-private and this widgets/route-layer
|
|
12
|
+
* boundary has no barrel re-export for it, the same accepted-duplication precedent `pages.ts`'s own
|
|
13
|
+
* `collectionEntryFieldValue` doc already documents for this exact kind of small formatting helper.
|
|
14
|
+
* A malformed timestamp degrades to the raw ISO string rather than "Invalid Date".
|
|
15
|
+
* @complexity O(1). */
|
|
16
|
+
function formatEntryDate(iso) {
|
|
17
|
+
const parsed = new Date(iso);
|
|
18
|
+
if (Number.isNaN(parsed.getTime()))
|
|
19
|
+
return iso;
|
|
20
|
+
return parsed.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
21
|
+
}
|
|
22
|
+
/** Legacy (no-`collection`) path: no single content type is in scope, so there is no field list to
|
|
23
|
+
* display — matches today's rendering exactly (title only). @complexity O(1). */
|
|
24
|
+
function toLegacyItemProps(entry, collectionList) {
|
|
25
|
+
const dateIso = entryDateIso(entry);
|
|
26
|
+
return { title: entry.title, href: collectionList.entryPublicHref(entry.type, entry.slug), dateIso, dateLabel: formatEntryDate(dateIso), fields: NO_DISPLAY_FIELDS };
|
|
27
|
+
}
|
|
28
|
+
/** Route-layer twin of `repo.sqlite.ts`'s `json_extract($.ext.site.<name>)` / `trash-aware-memory-
|
|
29
|
+
* repo.ts`'s own private `siteFieldValue` — duplicated for the same module-boundary reason
|
|
30
|
+
* `pages.ts`'s `collectionEntryFieldValue` already documents (no barrel re-export of either
|
|
31
|
+
* internal helper). @complexity O(1). */
|
|
32
|
+
function entryFieldValue(entry, name) {
|
|
33
|
+
const fields = entry.fieldsJson;
|
|
34
|
+
return fields?.ext?.site?.[name];
|
|
35
|
+
}
|
|
36
|
+
/** `collection`-path item: the content type's resolved `display.fields` (already validated/ordered
|
|
37
|
+
* by `parseCollectionListConfig`) become this entry's shown field values.
|
|
38
|
+
* @complexity O(f) over the displayed field count. */
|
|
39
|
+
function toCollectionItemProps(entry, fields, collectionList) {
|
|
40
|
+
const dateIso = entryDateIso(entry);
|
|
41
|
+
return {
|
|
42
|
+
title: entry.title,
|
|
43
|
+
href: collectionList.entryPublicHref(entry.type, entry.slug),
|
|
44
|
+
dateIso,
|
|
45
|
+
dateLabel: formatEntryDate(dateIso),
|
|
46
|
+
fields: fields.map((f) => ({ name: f.name, label: collectionList.humanizeFieldName(f.name), kind: f.kind, value: entryFieldValue(entry, f.name) })),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const LEGACY_DEFAULT_LAYOUT = "list";
|
|
50
|
+
const LEGACY_MIN_COLUMNS = 1;
|
|
51
|
+
const LEGACY_MAX_COLUMNS = 6;
|
|
52
|
+
const LEGACY_DEFAULT_COLUMNS = 3;
|
|
53
|
+
/** Display-only, unvalidated-against-any-content-type knobs — new, additive keys an old config never
|
|
54
|
+
* set, so absence falls back to the pre-existing visual shape (D7). `widgetLayout` applies on BOTH
|
|
55
|
+
* paths: the widget's layout default is "list" even with `collection` set (D7), unlike the
|
|
56
|
+
* collection marker's own "cards" default that `parseCollectionListConfig` applies.
|
|
57
|
+
* @complexity O(1). */
|
|
58
|
+
function widgetLayout(config, collectionList) {
|
|
59
|
+
return collectionList.isCollectionListLayout(config.layout) ? config.layout : LEGACY_DEFAULT_LAYOUT;
|
|
60
|
+
}
|
|
61
|
+
function legacyColumns(config) {
|
|
62
|
+
const raw = config.columns;
|
|
63
|
+
if (typeof raw !== "number" || !Number.isFinite(raw))
|
|
64
|
+
return LEGACY_DEFAULT_COLUMNS;
|
|
65
|
+
return Math.min(Math.max(Math.floor(raw), LEGACY_MIN_COLUMNS), LEGACY_MAX_COLUMNS);
|
|
66
|
+
}
|
|
67
|
+
function toItemIr(props) {
|
|
68
|
+
// Cast: RecentEntryItemProps is a plain, JSON-serializable shape with no index signature of its
|
|
69
|
+
// own — same precedent as menu.ts's toMenuItemProps for ResolvedNavItem.
|
|
70
|
+
return { componentId: "entry-list-item", props: props };
|
|
71
|
+
}
|
|
72
|
+
function collectionKey(config) {
|
|
73
|
+
return typeof config.collection === "string" && config.collection.length > 0 ? config.collection : undefined;
|
|
74
|
+
}
|
|
75
|
+
/** Resolves the shared legacy-path batch query once, then slices/maps it per instance — REQ-24's
|
|
76
|
+
* "one query per batch" contract, unchanged by this plan. @complexity O(n) over the shared result
|
|
77
|
+
* per instance, n bounded by `registryMax`. */
|
|
78
|
+
async function resolveLegacyInstances(deps, instances, workspaceId, registryMax, results) {
|
|
79
|
+
if (instances.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
// Review fix 3b: SYSTEM_CONTENT_TYPES is excluded IN the query (via
|
|
82
|
+
// listByWorkspaceExcludingTypes), not filtered out of an already limit-bounded result — a batch
|
|
83
|
+
// of system rows newer than real content used to be able to fill the bounded query and leave
|
|
84
|
+
// nothing (or fewer than asked for) once filtered afterward.
|
|
85
|
+
const visible = await deps.entryList.listByWorkspaceExcludingTypes({
|
|
86
|
+
workspaceId,
|
|
87
|
+
excludeTypes: deps.collectionList.systemContentTypes,
|
|
88
|
+
status: "published",
|
|
89
|
+
orderBy: "updatedAt",
|
|
90
|
+
orderDirection: "desc",
|
|
91
|
+
limit: registryMax,
|
|
92
|
+
});
|
|
93
|
+
for (const instance of instances) {
|
|
94
|
+
const configuredMax = typeof instance.config.maxItems === "number" ? instance.config.maxItems : registryMax;
|
|
95
|
+
const max = Math.max(0, Math.min(configuredMax, registryMax));
|
|
96
|
+
const entries = visible.slice(0, max);
|
|
97
|
+
results.set(instance.id, {
|
|
98
|
+
ok: true,
|
|
99
|
+
ir: {
|
|
100
|
+
componentId: "recent-entries",
|
|
101
|
+
props: { layout: widgetLayout(instance.config, deps.collectionList), columns: legacyColumns(instance.config), typeKey: "recent-entries" },
|
|
102
|
+
children: entries.map((entry) => toItemIr(toLegacyItemProps(entry, deps.collectionList))),
|
|
103
|
+
},
|
|
104
|
+
dependencyKeys: entries.map((entry) => entry.id),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Resolves one `collection`-configured instance independently — each may name a different content
|
|
109
|
+
* type/filter/sort, so (unlike the legacy path) there is no single shared query to batch across
|
|
110
|
+
* instances. @complexity One `contentTypes.findByKey` plus, only once that and the config both
|
|
111
|
+
* resolve, one bounded `listPublishedForDisplay` query (never an unbounded scan). */
|
|
112
|
+
async function resolveCollectionInstance(deps, instance, workspaceId, registryMax, typeKey) {
|
|
113
|
+
const contentType = await deps.contentTypes.findByKey({ workspaceId, key: typeKey });
|
|
114
|
+
if (contentType === null) {
|
|
115
|
+
return { ok: false, reason: "target-disabled" };
|
|
116
|
+
}
|
|
117
|
+
const parsed = deps.collectionList.parseCollectionListConfig(instance.config, contentType, { limitKey: "maxItems", defaultSort: "updated" });
|
|
118
|
+
if (!parsed.ok) {
|
|
119
|
+
return { ok: false, reason: "invalid-config" };
|
|
120
|
+
}
|
|
121
|
+
// REQ-25 defense in depth: the parser's own ceiling (24) is wider than this registration's (20).
|
|
122
|
+
const boundedQuery = { ...parsed.query, limit: Math.min(parsed.query.limit, registryMax) };
|
|
123
|
+
const rows = await deps.entryList.listPublishedForDisplay({ workspaceId, query: boundedQuery });
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
ir: {
|
|
127
|
+
componentId: "recent-entries",
|
|
128
|
+
// D7: the widget's own layout default is "list" — `parsed.display.layout` carries the
|
|
129
|
+
// collection MARKER's "cards" default for an absent key, so it is not used here.
|
|
130
|
+
// `structuredData` only here, never on the legacy path: that one lists mixed types, so it can
|
|
131
|
+
// never truthfully claim to be an FAQ (AW-7).
|
|
132
|
+
props: {
|
|
133
|
+
layout: widgetLayout(instance.config, deps.collectionList),
|
|
134
|
+
columns: parsed.display.columns,
|
|
135
|
+
typeKey,
|
|
136
|
+
...(parsed.display.structuredData === undefined ? {} : { structuredData: parsed.display.structuredData }),
|
|
137
|
+
},
|
|
138
|
+
children: rows.map((row) => toItemIr(toCollectionItemProps(row, parsed.display.fields, deps.collectionList))),
|
|
139
|
+
},
|
|
140
|
+
dependencyKeys: rows.map((row) => row.id),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export function createRecentEntriesResolver(deps, _optional = {}) {
|
|
144
|
+
return {
|
|
145
|
+
async resolveMany(instances, context) {
|
|
146
|
+
// `"recent-entries"` is a literal, so `getWidgetTypeRegistration` returns THIS registration's
|
|
147
|
+
// own literal `clamps.maxItems: number` here, never the shared interface's optional form.
|
|
148
|
+
const registryMax = getWidgetTypeRegistration({ typeKey: "recent-entries" }).clamps.maxItems;
|
|
149
|
+
const results = new Map();
|
|
150
|
+
const legacyInstances = instances.filter((instance) => collectionKey(instance.config) === undefined);
|
|
151
|
+
const collectionInstances = instances.filter((instance) => collectionKey(instance.config) !== undefined);
|
|
152
|
+
await resolveLegacyInstances(deps, legacyInstances, context.workspaceId, registryMax, results);
|
|
153
|
+
// Each collection-configured instance may name a different content type/filter/sort — unlike
|
|
154
|
+
// the legacy batch above, there is no single shared query, so these resolve independently and
|
|
155
|
+
// concurrently (never one query per instance run serially).
|
|
156
|
+
await Promise.all(collectionInstances.map(async (instance) => {
|
|
157
|
+
// Safe: filtered by `collectionKey(instance.config) !== undefined` above.
|
|
158
|
+
const typeKey = collectionKey(instance.config);
|
|
159
|
+
results.set(instance.id, await resolveCollectionInstance(deps, instance, context.workspaceId, registryMax, typeKey));
|
|
160
|
+
}));
|
|
161
|
+
return results;
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=recent-entries.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TaxonomyListPort, TermListPort } from "../../taxonomy/index.js";
|
|
2
|
+
import type { PageHtmlEmbedRef } from "../html/html-embeds.js";
|
|
3
|
+
import type { WidgetRenderIR } from "../types.js";
|
|
4
|
+
/** Workspace-scoped read ports are supplied by the host, like the other HTML embed resolvers. */
|
|
5
|
+
export declare function resolveTaxonomyEmbeds({ refs, deps }: {
|
|
6
|
+
refs: readonly PageHtmlEmbedRef[];
|
|
7
|
+
deps: {
|
|
8
|
+
taxonomyRepo?: TaxonomyListPort;
|
|
9
|
+
termRepo?: TermListPort;
|
|
10
|
+
};
|
|
11
|
+
}, _optional?: {}): Promise<ReadonlyMap<string, WidgetRenderIR>>;
|
|
12
|
+
//# sourceMappingURL=taxonomy.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Workspace-scoped read ports are supplied by the host, like the other HTML embed resolvers. */
|
|
2
|
+
export async function resolveTaxonomyEmbeds({ refs, deps }, _optional = {}) {
|
|
3
|
+
const resolved = new Map();
|
|
4
|
+
if (!deps.taxonomyRepo || !deps.termRepo)
|
|
5
|
+
return resolved;
|
|
6
|
+
const taxonomies = (await deps.taxonomyRepo.list()).filter((taxonomy) => taxonomy.status !== "trash" && taxonomy.status !== "purged");
|
|
7
|
+
const termsById = new Map();
|
|
8
|
+
for (const ref of refs) {
|
|
9
|
+
const key = ref.id;
|
|
10
|
+
if (!key)
|
|
11
|
+
continue;
|
|
12
|
+
// Names are human-readable targets, but unlike slugs are not guaranteed unique.
|
|
13
|
+
// An ambiguous name is unresolved, never an arbitrary choice; real ids win.
|
|
14
|
+
const matches = taxonomies.filter((taxonomy) => taxonomy.name === key);
|
|
15
|
+
const taxonomy = taxonomies.find((candidate) => candidate.id === key) ?? (matches.length === 1 ? matches[0] : undefined);
|
|
16
|
+
if (!taxonomy)
|
|
17
|
+
continue;
|
|
18
|
+
let terms = termsById.get(taxonomy.id);
|
|
19
|
+
if (!terms) {
|
|
20
|
+
terms = await deps.termRepo.listByTaxonomy({ taxonomyId: taxonomy.id });
|
|
21
|
+
termsById.set(taxonomy.id, terms);
|
|
22
|
+
}
|
|
23
|
+
resolved.set(key, {
|
|
24
|
+
componentId: "taxonomy-list",
|
|
25
|
+
props: {
|
|
26
|
+
id: taxonomy.id, name: taxonomy.name, hierarchical: taxonomy.hierarchical,
|
|
27
|
+
terms: terms.filter((term) => term.taxonomyId === taxonomy.id && term.status !== "trash" && term.status !== "purged")
|
|
28
|
+
.map((term) => ({ id: term.id, name: term.name, parentId: term.parentId })),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=taxonomy.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { StorageKernel } from "@jini-ai/db/kernel";
|
|
2
|
+
import type { WidgetsDatabase, WidgetsTables } from "./tables.js";
|
|
3
|
+
import type { UUID } from "@jini-ai/core/primitives";
|
|
4
|
+
import type { WidgetRegionBindingRepoPort } from "../ports.js";
|
|
5
|
+
import type { WidgetRegionBindingRow, WidgetRegionKey } from "../types.js";
|
|
6
|
+
export declare class SqlWidgetRegionBindingRepo implements WidgetRegionBindingRepoPort {
|
|
7
|
+
protected readonly kernel: StorageKernel<WidgetsDatabase>;
|
|
8
|
+
protected readonly tables: WidgetsTables;
|
|
9
|
+
constructor({ kernel, tables }: {
|
|
10
|
+
kernel: StorageKernel<WidgetsDatabase>;
|
|
11
|
+
tables: WidgetsTables;
|
|
12
|
+
}, _optional?: Record<string, never>);
|
|
13
|
+
findByRegion(required: {
|
|
14
|
+
workspaceId: UUID;
|
|
15
|
+
regionKey: WidgetRegionKey;
|
|
16
|
+
}): Promise<WidgetRegionBindingRow | null>;
|
|
17
|
+
listByWorkspace(required: {
|
|
18
|
+
workspaceId: UUID;
|
|
19
|
+
}): Promise<WidgetRegionBindingRow[]>;
|
|
20
|
+
upsert(required: {
|
|
21
|
+
workspaceId: UUID;
|
|
22
|
+
regionKey: WidgetRegionKey;
|
|
23
|
+
areaEntryId: UUID;
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
}): Promise<WidgetRegionBindingRow>;
|
|
26
|
+
markInactive(required: {
|
|
27
|
+
workspaceId: UUID;
|
|
28
|
+
regionKey: WidgetRegionKey;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
/** Delete-then-insert in one kernel transaction, so a reader never sees the workspace half rebuilt. */
|
|
31
|
+
rebuildForWorkspace(required: {
|
|
32
|
+
workspaceId: UUID;
|
|
33
|
+
bindings: readonly WidgetRegionBindingRow[];
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** Borrow the host kernel through this repository's narrow SQL schema view; no connection work. */
|
|
37
|
+
export declare function widgetRegionBindingRepoFor<DB>({ kernel, tables }: {
|
|
38
|
+
kernel: StorageKernel<DB>;
|
|
39
|
+
tables: WidgetsTables;
|
|
40
|
+
}, _optional?: Record<string, never>): SqlWidgetRegionBindingRepo;
|
|
41
|
+
//# sourceMappingURL=repo.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file THE durable `WidgetRegionBindingRepoPort`: one Kysely query body for every dialect the
|
|
3
|
+
* storage kernel drives (storage plan §4, ADR-066). ADR-006 rule-of-two "second adapter" half —
|
|
4
|
+
* `repo.memory.ts`'s `InMemoryWidgetRegionBindingRepo` is the first; `repo.sqlite.ts` is the thin
|
|
5
|
+
* subclass the composition root builds from the content db handle. `upsert` targets the composite
|
|
6
|
+
* `UNIQUE(workspace_id, region_key)` index (a real strengthening of INV-02 over the in-memory
|
|
7
|
+
* adapter's single-process-only guarantee).
|
|
8
|
+
*
|
|
9
|
+
* Architectural role:
|
|
10
|
+
* Infrastructure adapter. No feature logic — `region-area-service.ts` owns every write decision;
|
|
11
|
+
* this class only persists what it's told.
|
|
12
|
+
*/
|
|
13
|
+
function toRecord(row) {
|
|
14
|
+
return {
|
|
15
|
+
workspaceId: row.workspace_id,
|
|
16
|
+
regionKey: row.region_key,
|
|
17
|
+
areaEntryId: row.area_entry_id,
|
|
18
|
+
updatedAt: row.updated_at,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function toRow(binding) {
|
|
22
|
+
return {
|
|
23
|
+
workspace_id: binding.workspaceId,
|
|
24
|
+
region_key: binding.regionKey,
|
|
25
|
+
area_entry_id: binding.areaEntryId,
|
|
26
|
+
updated_at: binding.updatedAt,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export class SqlWidgetRegionBindingRepo {
|
|
30
|
+
kernel;
|
|
31
|
+
tables;
|
|
32
|
+
constructor({ kernel, tables }, _optional = {}) { this.kernel = kernel; this.tables = tables; }
|
|
33
|
+
async findByRegion(required) {
|
|
34
|
+
const row = await this.kernel.run((db) => db
|
|
35
|
+
.selectFrom(this.tables.regionBindings)
|
|
36
|
+
.selectAll()
|
|
37
|
+
.where("workspace_id", "=", required.workspaceId)
|
|
38
|
+
.where("region_key", "=", required.regionKey)
|
|
39
|
+
.limit(1)
|
|
40
|
+
.executeTakeFirst());
|
|
41
|
+
return row ? toRecord(row) : null;
|
|
42
|
+
}
|
|
43
|
+
async listByWorkspace(required) {
|
|
44
|
+
const rows = await this.kernel.run((db) => db.selectFrom(this.tables.regionBindings).selectAll().where("workspace_id", "=", required.workspaceId).execute());
|
|
45
|
+
return rows.map(toRecord);
|
|
46
|
+
}
|
|
47
|
+
async upsert(required) {
|
|
48
|
+
const row = {
|
|
49
|
+
workspaceId: required.workspaceId,
|
|
50
|
+
regionKey: required.regionKey,
|
|
51
|
+
areaEntryId: required.areaEntryId,
|
|
52
|
+
updatedAt: required.updatedAt,
|
|
53
|
+
};
|
|
54
|
+
await this.kernel.run((db) => db
|
|
55
|
+
.insertInto(this.tables.regionBindings)
|
|
56
|
+
.values(toRow(row))
|
|
57
|
+
.onConflict((oc) => oc.columns(["workspace_id", "region_key"]).doUpdateSet({ area_entry_id: row.areaEntryId, updated_at: row.updatedAt }))
|
|
58
|
+
.execute());
|
|
59
|
+
return row;
|
|
60
|
+
}
|
|
61
|
+
async markInactive(required) {
|
|
62
|
+
await this.kernel.run((db) => db
|
|
63
|
+
.deleteFrom(this.tables.regionBindings)
|
|
64
|
+
.where("workspace_id", "=", required.workspaceId)
|
|
65
|
+
.where("region_key", "=", required.regionKey)
|
|
66
|
+
.execute());
|
|
67
|
+
}
|
|
68
|
+
/** Delete-then-insert in one kernel transaction, so a reader never sees the workspace half rebuilt. */
|
|
69
|
+
async rebuildForWorkspace(required) {
|
|
70
|
+
await this.kernel.transaction(async () => {
|
|
71
|
+
await this.kernel.run((db) => db.deleteFrom(this.tables.regionBindings).where("workspace_id", "=", required.workspaceId).execute());
|
|
72
|
+
if (required.bindings.length === 0)
|
|
73
|
+
return;
|
|
74
|
+
await this.kernel.run((db) => db.insertInto(this.tables.regionBindings).values(required.bindings.map(toRow)).execute());
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Borrow the host kernel through this repository's narrow SQL schema view; no connection work. */
|
|
79
|
+
export function widgetRegionBindingRepoFor({ kernel, tables }, _optional = {}) {
|
|
80
|
+
return new SqlWidgetRegionBindingRepo({ kernel: kernel, tables });
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=repo.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Region-binding table names are a required host choice. */
|
|
2
|
+
export interface WidgetsTables {
|
|
3
|
+
regionBindings: string;
|
|
4
|
+
}
|
|
5
|
+
/** Local narrow SQL row; widgets never imports a host-generated database schema. */
|
|
6
|
+
export interface WidgetRegionBindingsTable {
|
|
7
|
+
workspace_id: string;
|
|
8
|
+
region_key: string;
|
|
9
|
+
area_entry_id: string;
|
|
10
|
+
updated_at: string;
|
|
11
|
+
}
|
|
12
|
+
export type WidgetsDatabase = Record<string, WidgetRegionBindingsTable>;
|
|
13
|
+
//# sourceMappingURL=tables.d.ts.map
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Core type definitions for the Tovu `widgets` library (ADR-047, SPEC-043).
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* Widgets are placeable, reusable, individually-configured components (a contact
|
|
6
|
+
* form, a recent-posts list, a text block, social links, a menu) placed either
|
|
7
|
+
* into a theme-declared region (an ordered list, backed by a seeded
|
|
8
|
+
* `widget_area` entry) or embedded inline inside a page's rich-text body (a
|
|
9
|
+
* `widgetEmbed` TipTap node). A widget instance is a seeded ADR-022 content
|
|
10
|
+
* entry (type `widget`), exactly as a menu is (ADR-029) — this file mirrors
|
|
11
|
+
* `navigation/types.ts` deliberately, per the ADR-047 debate's finding that the
|
|
12
|
+
* region-binding mechanism must structurally match `nav_location_bindings`
|
|
13
|
+
* (source-of-truth-on-the-entry + a derived, reconciled binding index), not
|
|
14
|
+
* merely resemble it.
|
|
15
|
+
*
|
|
16
|
+
* How it relates to the project:
|
|
17
|
+
* - `core/ports.ts` supplies the shared primitives (`UUID`, `JsonObject`, …).
|
|
18
|
+
* - `widgets/ports.ts` declares the one rule-of-two port this library adds
|
|
19
|
+
* (`WidgetRegionBindingRepoPort`, mirroring `NavLocationBindingRepoPort`).
|
|
20
|
+
* - `widgets/registry.ts` declares the v1 widget-type registry as plain data —
|
|
21
|
+
* see that file for why type *registration* and type *behavior* are split.
|
|
22
|
+
* - The theme renderer never resolves a widget reference itself (ADR-020 §7 /
|
|
23
|
+
* ADR-029 §7's "theme receives resolved data" boundary, unchanged here).
|
|
24
|
+
*
|
|
25
|
+
* Architectural role:
|
|
26
|
+
* INTERFACES + TYPES ONLY (no feature logic). This is the design-frozen shape
|
|
27
|
+
* ADR-047 (as amended by its 2026-07-21 debate fold-in) introduces; the entries
|
|
28
|
+
* repo, revisions, and the command gateway are reused unchanged from ADR-022 /
|
|
29
|
+
* ADR-008.
|
|
30
|
+
*/
|
|
31
|
+
import type { ISODateTime, JsonObject, UUID } from "@jini-ai/core/primitives";
|
|
32
|
+
/** `widget` ships as a seeded content-type registry row (ADR-022 §1), like `post`/`page`/`menu`. */
|
|
33
|
+
export declare const WIDGET_CONTENT_TYPE: "widget";
|
|
34
|
+
/**
|
|
35
|
+
* `widget_area` ships as a seeded, **system-managed** content-type registry
|
|
36
|
+
* row — never publicly routable, never itself placeable as a widget, never a
|
|
37
|
+
* valid `widgetEmbed` target (REQ-17 — no recursion into a region from inside
|
|
38
|
+
* a region).
|
|
39
|
+
*/
|
|
40
|
+
export declare const WIDGET_AREA_CONTENT_TYPE: "widget_area";
|
|
41
|
+
/** The extension-field owner namespace for widget-level fields (`fields.ext.widget.*`). */
|
|
42
|
+
export declare const WIDGET_FIELD_NAMESPACE: "widget";
|
|
43
|
+
/** The extension-field owner namespace for a `widget_area` entry's region assignment. */
|
|
44
|
+
export declare const WIDGET_AREA_FIELD_NAMESPACE: "widgets";
|
|
45
|
+
/**
|
|
46
|
+
* The v1 widget-type keys (SPEC-043 REQ-09). Kept as a closed union rather than
|
|
47
|
+
* a bare `string` so a typo in a registration or a call site is a compile error,
|
|
48
|
+
* not a runtime `unknown-type` surprise.
|
|
49
|
+
*/
|
|
50
|
+
export type WidgetTypeKey = "text" | "social-links" | "recent-entries" | "menu" | "contact-form";
|
|
51
|
+
/**
|
|
52
|
+
* A widget type's declared capability class (ADR-047 Debate Fold-In Amendment
|
|
53
|
+
* 3). `static` types render validated config directly with no resolver;
|
|
54
|
+
* `query`/`form`/`entry-reference` types have a registered resolver in the
|
|
55
|
+
* closed `CORE_RESOLVERS` map (`resolvers/index.ts`).
|
|
56
|
+
*/
|
|
57
|
+
export type WidgetCapability = "static" | "query" | "form" | "entry-reference";
|
|
58
|
+
/** Where a widget type may legally be placed. */
|
|
59
|
+
export type WidgetPlacementContext = "region" | "inline";
|
|
60
|
+
/**
|
|
61
|
+
* A widget type's **registration** — plain, JSON-serializable data (SPEC-043
|
|
62
|
+
* REQ-07). Never imports or references executable behavior; the only pointer
|
|
63
|
+
* to code this record carries is `resolverId`, which must resolve *only*
|
|
64
|
+
* through the closed `CORE_RESOLVERS` map (REQ-08) — never a module path,
|
|
65
|
+
* function name, query string, or expression.
|
|
66
|
+
*/
|
|
67
|
+
export interface WidgetTypeRegistration {
|
|
68
|
+
readonly typeKey: WidgetTypeKey;
|
|
69
|
+
readonly capability: WidgetCapability;
|
|
70
|
+
/** JSON-Schema-shaped description of `fields.ext.widget.*`'s valid shape for this type. */
|
|
71
|
+
readonly configSchema: JsonObject;
|
|
72
|
+
readonly placementContexts: readonly WidgetPlacementContext[];
|
|
73
|
+
/** Cost clamps enforced by core at the orchestration layer, never by the resolver's own discipline (REQ-25). */
|
|
74
|
+
readonly clamps: {
|
|
75
|
+
readonly maxItems?: number;
|
|
76
|
+
readonly timeoutMs: number;
|
|
77
|
+
};
|
|
78
|
+
/** `undefined` for `static`-capability types (REQ-10). Indexes into `CORE_RESOLVERS` only. */
|
|
79
|
+
readonly resolverId?: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* `purged` is observably distinct from `trash` (REQ-43): both are treated identically everywhere a
|
|
83
|
+
* widget instance's availability is checked (resolution, placement validation — see
|
|
84
|
+
* `resolver-service.ts`/`region-area-service.ts`), but only `purged` records that the instance was
|
|
85
|
+
* force-deleted past a known reference (`purgeWidgetInstance({ force: true })`), vs. an ordinary
|
|
86
|
+
* `trash` that may still be restored with no prior reference conflict. No real hard-delete exists in
|
|
87
|
+
* this codebase for any content type (`EntryRepoPort` has no delete method) — this status is the
|
|
88
|
+
* cheap, in-model way to keep that distinction visible without one.
|
|
89
|
+
*
|
|
90
|
+
* No `draft` member (Fable adversarial-review fix, 2026-07-21, Finding F) — `write-service.ts`
|
|
91
|
+
* creates every widget instance directly as `active`; nothing anywhere in this codebase ever writes
|
|
92
|
+
* `draft`. A prior version of this union carried it anyway, which was a live landmine: the
|
|
93
|
+
* resolver/placement-validation skip lists above only ever checked for `trash`/`purged`, so a
|
|
94
|
+
* `draft` status — had anything ever produced one — would have rendered publicly instead of being
|
|
95
|
+
* held back. Removed rather than defensively added to the skip lists, since there is no real
|
|
96
|
+
* "draft" concept for widget instances to guard.
|
|
97
|
+
*/
|
|
98
|
+
export type WidgetInstanceStatus = "active" | "trash" | "purged";
|
|
99
|
+
/**
|
|
100
|
+
* A typed read model over a `type='widget'` entries row (ADR-022 §2 universal
|
|
101
|
+
* columns) plus the parsed `fields.ext.widget.*` config bag. A projection, not
|
|
102
|
+
* a new table — widget instances are entries (REQ-01).
|
|
103
|
+
*/
|
|
104
|
+
export interface WidgetInstanceEntry {
|
|
105
|
+
readonly id: UUID;
|
|
106
|
+
readonly workspaceId: UUID;
|
|
107
|
+
readonly slug: string;
|
|
108
|
+
readonly title: string;
|
|
109
|
+
readonly status: WidgetInstanceStatus;
|
|
110
|
+
readonly widgetType: WidgetTypeKey;
|
|
111
|
+
/** Validated against the type's registered `configSchema` on every write (REQ-02). */
|
|
112
|
+
readonly config: JsonObject;
|
|
113
|
+
readonly updatedAt: ISODateTime;
|
|
114
|
+
readonly version: number;
|
|
115
|
+
}
|
|
116
|
+
/** A theme-registered region key (e.g. `header`, `footer`, `sidebar`). */
|
|
117
|
+
export type WidgetRegionKey = string;
|
|
118
|
+
/** One entry in a `widget_area`'s ordered placement list (lives in the area entry's `bodyJson`). */
|
|
119
|
+
export interface WidgetPlacementNode {
|
|
120
|
+
/** Stable ULID, minted once, preserved across reorders — for diagnostics and `entry_refs` locators. */
|
|
121
|
+
readonly placementId: UUID;
|
|
122
|
+
readonly widgetEntryId: UUID;
|
|
123
|
+
readonly enabled: boolean;
|
|
124
|
+
}
|
|
125
|
+
/** The full region-composition document stored in a `widget_area` entry's `bodyJson`. */
|
|
126
|
+
export interface WidgetAreaDoc {
|
|
127
|
+
readonly schemaVersion: number;
|
|
128
|
+
readonly placements: readonly WidgetPlacementNode[];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A typed read model over a `type='widget_area'` entries row. `regionKey` is
|
|
132
|
+
* the **source of truth** for which region this area fills
|
|
133
|
+
* (`fields.ext.widgets.regionKey`) — mirrors `NavMenuEntry.locations` exactly
|
|
134
|
+
* (ADR-047 Debate Fold-In Amendment 1).
|
|
135
|
+
*/
|
|
136
|
+
export interface WidgetAreaEntry {
|
|
137
|
+
readonly id: UUID;
|
|
138
|
+
readonly workspaceId: UUID;
|
|
139
|
+
readonly regionKey: WidgetRegionKey;
|
|
140
|
+
readonly doc: WidgetAreaDoc;
|
|
141
|
+
readonly updatedAt: ISODateTime;
|
|
142
|
+
readonly version: number;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A row of the derived `widget_region_bindings` index. **Derived + rebuildable**
|
|
146
|
+
* from `WidgetAreaEntry.regionKey` at the write chokepoint (same species as
|
|
147
|
+
* `nav_location_bindings` and ADR-022 §5 `entry_refs`), NOT a source of truth
|
|
148
|
+
* and NOT revision-generating (INV-02). Reconciled by exactly one code path
|
|
149
|
+
* (`region-area-service.ts`'s `reconcileWidgetRegionBindings`) — never
|
|
150
|
+
* hand-authored.
|
|
151
|
+
*/
|
|
152
|
+
export interface WidgetRegionBindingRow {
|
|
153
|
+
readonly workspaceId: UUID;
|
|
154
|
+
readonly regionKey: WidgetRegionKey;
|
|
155
|
+
readonly areaEntryId: UUID;
|
|
156
|
+
readonly updatedAt: ISODateTime;
|
|
157
|
+
}
|
|
158
|
+
/** A theme/plugin-registered widget region (mirrors `NavLocationDescriptor`). */
|
|
159
|
+
export interface WidgetRegionDescriptor {
|
|
160
|
+
readonly key: WidgetRegionKey;
|
|
161
|
+
readonly label: string;
|
|
162
|
+
readonly registeredBy: string;
|
|
163
|
+
readonly description?: string;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The `widgetEmbed` node's persisted shape. A block-level atom (REQ-18) — never
|
|
167
|
+
* nested inside a widget instance's own `bodyJson` (REQ-19, INV-04), enforced
|
|
168
|
+
* by `embed-validation.ts`'s `validateWidgetEmbedMutation`, the single
|
|
169
|
+
* validator both the live-editor path and the server-side/AI path call.
|
|
170
|
+
*/
|
|
171
|
+
export interface WidgetEmbedNode {
|
|
172
|
+
readonly type: "widgetEmbed";
|
|
173
|
+
readonly placementId: UUID;
|
|
174
|
+
readonly widgetEntryId: UUID;
|
|
175
|
+
}
|
|
176
|
+
/** The internal render IR node shape every resolved widget produces (ADR-020's canonical render IR). */
|
|
177
|
+
export interface WidgetRenderIR {
|
|
178
|
+
readonly componentId: string;
|
|
179
|
+
readonly props: JsonObject;
|
|
180
|
+
readonly children?: readonly WidgetRenderIR[];
|
|
181
|
+
}
|
|
182
|
+
/** Context passed to a resolver for one page render (REQ-23). */
|
|
183
|
+
export interface WidgetResolveContext {
|
|
184
|
+
readonly workspaceId: UUID;
|
|
185
|
+
readonly locale?: string;
|
|
186
|
+
readonly preview: boolean;
|
|
187
|
+
}
|
|
188
|
+
/** A minimal, read-only view of a widget instance passed into a resolver — never a raw DB handle (SPEC-043's REQ-24 batching contract). */
|
|
189
|
+
export interface WidgetInstanceView {
|
|
190
|
+
readonly id: UUID;
|
|
191
|
+
readonly widgetType: WidgetTypeKey;
|
|
192
|
+
readonly config: JsonObject;
|
|
193
|
+
}
|
|
194
|
+
/** Why a widget resolution failed (SPEC-043 REQ-27's closed failure taxonomy). */
|
|
195
|
+
export type WidgetResolveFailureReason = "unknown-type" | "invalid-config" | "target-disabled" | "timeout" | "resolver-error";
|
|
196
|
+
export type WidgetResolveResult = {
|
|
197
|
+
readonly ok: true;
|
|
198
|
+
readonly ir: WidgetRenderIR;
|
|
199
|
+
readonly dependencyKeys: readonly string[];
|
|
200
|
+
} | {
|
|
201
|
+
readonly ok: false;
|
|
202
|
+
readonly reason: WidgetResolveFailureReason;
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* The typed resolver contract every dynamic (non-`static`) widget type
|
|
206
|
+
* implements (REQ-23/24). **Batch-first by design** — `resolveMany` is called
|
|
207
|
+
* at most once per type per page render (REQ-24), never once per placement, so
|
|
208
|
+
* N+1 query cost is a contract violation, not an optimization opportunity.
|
|
209
|
+
*/
|
|
210
|
+
export interface WidgetResolver {
|
|
211
|
+
resolveMany(instances: readonly WidgetInstanceView[], context: WidgetResolveContext): Promise<ReadonlyMap<UUID, WidgetResolveResult>>;
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Content-type identity
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
/** `widget` ships as a seeded content-type registry row (ADR-022 §1), like `post`/`page`/`menu`. */
|
|
5
|
+
export const WIDGET_CONTENT_TYPE = "widget";
|
|
6
|
+
/**
|
|
7
|
+
* `widget_area` ships as a seeded, **system-managed** content-type registry
|
|
8
|
+
* row — never publicly routable, never itself placeable as a widget, never a
|
|
9
|
+
* valid `widgetEmbed` target (REQ-17 — no recursion into a region from inside
|
|
10
|
+
* a region).
|
|
11
|
+
*/
|
|
12
|
+
export const WIDGET_AREA_CONTENT_TYPE = "widget_area";
|
|
13
|
+
/** The extension-field owner namespace for widget-level fields (`fields.ext.widget.*`). */
|
|
14
|
+
export const WIDGET_FIELD_NAMESPACE = "widget";
|
|
15
|
+
/** The extension-field owner namespace for a `widget_area` entry's region assignment. */
|
|
16
|
+
export const WIDGET_AREA_FIELD_NAMESPACE = "widgets";
|
|
17
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file REQ-34's "where used" projection for widgets — the model-facing shape every caller that
|
|
3
|
+
* surfaces reference data renders (the widget-editor banner, the purge-blocked 409 body, the
|
|
4
|
+
* `widgets.diagnose` AI tool), so no consumer has to special-case where the data came from.
|
|
5
|
+
*
|
|
6
|
+
* Architectural role:
|
|
7
|
+
* This is a response SHAPER, not transport. It maps `entry_refs` rows — this domain's own
|
|
8
|
+
* reference-integrity index (`core/entry-refs`) — onto the disclosure vocabulary widgets speaks
|
|
9
|
+
* (`region` | `embed`). It touches no `Response`, no status code, and no error mapping; those stay
|
|
10
|
+
* in the HTTP layer.
|
|
11
|
+
*
|
|
12
|
+
* Why it lives here rather than under the HTTP admin layer, where it used to:
|
|
13
|
+
* it was the single import edge that made `widgets` depend on the composition root, and `server/`
|
|
14
|
+
* transitively reaches the whole tree — that one edge inflated this domain's transitive closure
|
|
15
|
+
* from 47 files / 7 modules to 162 files / 31 modules, which is the entire reason widgets looked
|
|
16
|
+
* unextractable. A host importing a domain's projection is the correct direction; a domain
|
|
17
|
+
* importing its host's transport module is not. Same misplacement species as
|
|
18
|
+
* `core/rate-limit/rate-limit.ts` and `features/plugin-runtime/admin-response.ts`, both already
|
|
19
|
+
* relocated. `server/http/site/page-head.ts` is the same species but NOT relocated — its own doc
|
|
20
|
+
* comment records a deliberate ADR-032 decision that the render layer, not `seo`, owns this seam,
|
|
21
|
+
* which the 2026-08-02 module-graph analysis's "page-head.ts -> seo/" suggestion did not account
|
|
22
|
+
* for; that one needs an architecture decision, not a mechanical move (2026-08-12 Refactor pass).
|
|
23
|
+
* The HTTP layer re-exports this symbol so its own consumers keep their existing import site.
|
|
24
|
+
*/
|
|
25
|
+
import type { EntryRefRow } from "./ports.js";
|
|
26
|
+
/** One disclosed reference: which widgets-level location holds it, and where inside that source. */
|
|
27
|
+
export interface WhereUsedReference {
|
|
28
|
+
/** `entry_refs` distinguishes several source kinds; widgets discloses them as exactly two. */
|
|
29
|
+
kind: "region" | "embed";
|
|
30
|
+
sourceEntryId: string;
|
|
31
|
+
fieldPath: string;
|
|
32
|
+
}
|
|
33
|
+
/** REQ-34's where-used disclosure shape. */
|
|
34
|
+
export interface WhereUsedResponse {
|
|
35
|
+
count: number;
|
|
36
|
+
references: WhereUsedReference[];
|
|
37
|
+
}
|
|
38
|
+
export declare function toWhereUsedResponse({ refs }: {
|
|
39
|
+
refs: readonly EntryRefRow[];
|
|
40
|
+
}, _optional?: Record<string, never>): WhereUsedResponse;
|
|
41
|
+
//# sourceMappingURL=where-used.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function toWhereUsedResponse({ refs }, _optional = {}) {
|
|
2
|
+
return {
|
|
3
|
+
count: refs.length,
|
|
4
|
+
references: refs.map((ref) => ({
|
|
5
|
+
kind: ref.sourceKind === "widget-embed" ? "embed" : "region",
|
|
6
|
+
sourceEntryId: ref.sourceEntryId,
|
|
7
|
+
fieldPath: ref.fieldPath,
|
|
8
|
+
})),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=where-used.js.map
|