@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,180 @@
|
|
|
1
|
+
import { nowIso as clockNowIso } from "@jini-ai/core/primitives";
|
|
2
|
+
import { PRE_AUTHORIZED, requireWidgetPermission, WIDGETS_SYSTEM_ACTOR_ID, } from "./authorize-helper.js";
|
|
3
|
+
import { withEntryLock } from "./concurrency.js";
|
|
4
|
+
import { areaDocWithPlacements, buildWidgetAreaFieldsJson, emptyWidgetAreaDoc, ensureWidgetContentTypesRegistered, parseWidgetAreaPayload, parseWidgetInstancePayload, toWidgetAreaEntry, widgetAreaSlug, } from "./entry-payload.js";
|
|
5
|
+
import { WidgetAreaConflictError, WidgetAreaNotFoundError, WidgetInstanceNotFoundError, } from "./errors.js";
|
|
6
|
+
import { WIDGET_AREA_CONTENT_TYPE, WIDGET_AREA_FIELD_NAMESPACE, WIDGET_CONTENT_TYPE, } from "./types.js";
|
|
7
|
+
async function extractAndStoreAreaRefs(deps, workspaceId, entry) {
|
|
8
|
+
const payload = parseWidgetAreaPayload({ fieldsJson: entry.fieldsJson });
|
|
9
|
+
const refs = deps.host.entryRefs.extract({
|
|
10
|
+
workspaceId,
|
|
11
|
+
sourceEntryId: entry.id,
|
|
12
|
+
sourceEntryType: WIDGET_AREA_CONTENT_TYPE,
|
|
13
|
+
bodyJson: payload.doc,
|
|
14
|
+
fieldsExt: { [WIDGET_AREA_FIELD_NAMESPACE]: { regionKey: payload.regionKey } },
|
|
15
|
+
});
|
|
16
|
+
await deps.entryRefsRepo.replaceForSource({ workspaceId, sourceEntryId: entry.id, refs });
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The one shared shape both `createEntry` (`bindWidgetArea`) and `updateEntry`
|
|
20
|
+
* (`mutateWidgetAreaPlacements`) below need. Bridges `deps.outbox` (the raw, full-`DomainEvent`
|
|
21
|
+
* infra port) through `toEntryOutbox` into the narrower `{enqueue({name,payload})}` shape those
|
|
22
|
+
* chokepoints declare locally — previously `deps.outbox` was forwarded unwrapped here, which
|
|
23
|
+
* compiled but threw `NOT NULL constraint failed: outbox_events.id` against the real SQLite outbox
|
|
24
|
+
* on every region bind/placement-mutation. Both call sites share this one helper already, so fixing
|
|
25
|
+
* it here fixes both at once — see `write-service.ts`'s identically-named, identically-reasoned
|
|
26
|
+
* helper for the full investigation pointer.
|
|
27
|
+
*/
|
|
28
|
+
function entriesWriteDeps(deps, workspaceId) {
|
|
29
|
+
return {
|
|
30
|
+
entryRepo: deps.entryRepo,
|
|
31
|
+
contentTypeRepo: deps.contentTypeRepo,
|
|
32
|
+
clock: deps.clock,
|
|
33
|
+
ids: deps.ids,
|
|
34
|
+
authorize: PRE_AUTHORIZED,
|
|
35
|
+
outbox: deps.host.entries.toEntryOutbox({ outbox: deps.outbox, clock: deps.clock, idGen: deps.ids, workspaceId }),
|
|
36
|
+
onWritten: (entry) => extractAndStoreAreaRefs(deps, workspaceId, entry),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* REQ-13: seeds a `widget_area` entry for a region with no existing binding (called on theme
|
|
41
|
+
* activation — a system/boot operation, not a user-initiated `widgets.place` mutation, so no
|
|
42
|
+
* `authorize()` gate here, matching how `navigation`'s location seeding is likewise boot-driven).
|
|
43
|
+
* Idempotent: a region already bound (or an existing-but-unbound `widget_area` entry for that
|
|
44
|
+
* region key, found via its deterministic slug) is reused, never duplicated.
|
|
45
|
+
*/
|
|
46
|
+
export async function bindWidgetArea(required, _optional = {}) {
|
|
47
|
+
const { deps, input } = required;
|
|
48
|
+
await ensureWidgetContentTypesRegistered({ deps, workspaceId: input.workspaceId });
|
|
49
|
+
return withEntryLock({ key: `${input.workspaceId}::area::${input.regionKey}`, fn: async () => {
|
|
50
|
+
const existingBinding = await deps.bindingRepo.findByRegion({ workspaceId: input.workspaceId, regionKey: input.regionKey });
|
|
51
|
+
if (existingBinding) {
|
|
52
|
+
const boundEntry = await deps.entryRepo.findById({ workspaceId: input.workspaceId, id: existingBinding.areaEntryId });
|
|
53
|
+
if (boundEntry)
|
|
54
|
+
return { areaEntry: toWidgetAreaEntry({ entry: boundEntry }) };
|
|
55
|
+
}
|
|
56
|
+
const slug = widgetAreaSlug({ regionKey: input.regionKey });
|
|
57
|
+
let entry = await deps.entryRepo.findBySlug({ workspaceId: input.workspaceId, type: WIDGET_AREA_CONTENT_TYPE, slug });
|
|
58
|
+
if (!entry) {
|
|
59
|
+
const created = await deps.host.entries.createEntry({
|
|
60
|
+
deps: entriesWriteDeps(deps, input.workspaceId),
|
|
61
|
+
input: {
|
|
62
|
+
actorId: WIDGETS_SYSTEM_ACTOR_ID,
|
|
63
|
+
workspaceId: input.workspaceId,
|
|
64
|
+
type: WIDGET_AREA_CONTENT_TYPE,
|
|
65
|
+
slug,
|
|
66
|
+
title: `Region: ${input.regionKey}`,
|
|
67
|
+
fieldsJson: buildWidgetAreaFieldsJson({ payload: { regionKey: input.regionKey, doc: emptyWidgetAreaDoc({}) } }),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
if (!created.ok)
|
|
71
|
+
throw created.error;
|
|
72
|
+
entry = created.value.entry;
|
|
73
|
+
}
|
|
74
|
+
await deps.bindingRepo.upsert({
|
|
75
|
+
workspaceId: input.workspaceId,
|
|
76
|
+
regionKey: input.regionKey,
|
|
77
|
+
areaEntryId: entry.id,
|
|
78
|
+
updatedAt: clockNowIso({ clock: deps.clock }),
|
|
79
|
+
});
|
|
80
|
+
return { areaEntry: toWidgetAreaEntry({ entry: entry }) };
|
|
81
|
+
} });
|
|
82
|
+
}
|
|
83
|
+
/** REQ-16/AC-10: every referenced widgetEntryId must exist, be a live (non-trashed) widget instance,
|
|
84
|
+
* in this same workspace — checked before any write. Throws the first violation found. */
|
|
85
|
+
export async function validatePlacementWidgetsExist({ deps, workspaceId, placements }, _optional = {}) {
|
|
86
|
+
for (const placement of placements) {
|
|
87
|
+
const widget = await deps.entryRepo.findById({ workspaceId, id: placement.widgetEntryId });
|
|
88
|
+
if (!widget || widget.type !== WIDGET_CONTENT_TYPE) {
|
|
89
|
+
throw new WidgetInstanceNotFoundError({ message: `placement references widget '${placement.widgetEntryId}', which does not exist in workspace '${workspaceId}' (REQ-16)` });
|
|
90
|
+
}
|
|
91
|
+
const widgetPayload = parseWidgetInstancePayload({ fieldsJson: widget.fieldsJson });
|
|
92
|
+
if (widgetPayload.status === "trash" || widgetPayload.status === "purged") {
|
|
93
|
+
throw new WidgetInstanceNotFoundError({ message: `placement references widget '${placement.widgetEntryId}', which is trashed (REQ-16)` });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Converts a rejected `updateEntry` result into the matching typed error — a version conflict
|
|
98
|
+
* reports the CURRENT (post-conflict) version, any other failure propagates as-is. Never returns. */
|
|
99
|
+
async function rejectMutateWidgetAreaPlacementsWrite(deps, workspaceId, areaEntryId, current, error) {
|
|
100
|
+
if (deps.host.entries.isVersionConflict(error)) {
|
|
101
|
+
const latest = await deps.entryRepo.findById({ workspaceId, id: areaEntryId });
|
|
102
|
+
throw new WidgetAreaConflictError({ message: error.message, currentVersion: latest?.version ?? current.version });
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
/** REQ-15/16: one atomic, version-guarded write; rejects placements referencing a nonexistent/trashed/cross-workspace widget. */
|
|
107
|
+
export async function mutateWidgetAreaPlacements(required, _optional = {}) {
|
|
108
|
+
const { deps, input } = required;
|
|
109
|
+
await requireWidgetPermission({
|
|
110
|
+
authorize: deps.authorize,
|
|
111
|
+
actor: input.actor,
|
|
112
|
+
workspaceId: input.workspaceId,
|
|
113
|
+
permission: "widgets.place",
|
|
114
|
+
});
|
|
115
|
+
return withEntryLock({ key: `${input.workspaceId}::${input.areaEntryId}`, fn: async () => {
|
|
116
|
+
const current = await deps.entryRepo.findById({ workspaceId: input.workspaceId, id: input.areaEntryId });
|
|
117
|
+
if (!current || current.type !== WIDGET_AREA_CONTENT_TYPE) {
|
|
118
|
+
throw new WidgetAreaNotFoundError({ message: `widget_area '${input.areaEntryId}' was not found` });
|
|
119
|
+
}
|
|
120
|
+
await validatePlacementWidgetsExist({ deps: deps, workspaceId: input.workspaceId, placements: input.placements });
|
|
121
|
+
const currentPayload = parseWidgetAreaPayload({ fieldsJson: current.fieldsJson });
|
|
122
|
+
const nextDoc = areaDocWithPlacements({ doc: currentPayload.doc, placements: input.placements });
|
|
123
|
+
const result = await deps.host.entries.updateEntry({
|
|
124
|
+
deps: entriesWriteDeps(deps, input.workspaceId),
|
|
125
|
+
input: {
|
|
126
|
+
actorId: input.actor.principalId,
|
|
127
|
+
workspaceId: input.workspaceId,
|
|
128
|
+
id: input.areaEntryId,
|
|
129
|
+
fieldsJson: buildWidgetAreaFieldsJson({ payload: { regionKey: currentPayload.regionKey, doc: nextDoc } }),
|
|
130
|
+
expectedVersion: input.baseVersion,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
if (!result.ok) {
|
|
134
|
+
return rejectMutateWidgetAreaPlacementsWrite(deps, input.workspaceId, input.areaEntryId, current, result.error);
|
|
135
|
+
}
|
|
136
|
+
// regionKey never changes on a placement mutation — refresh the derived row's audit timestamp
|
|
137
|
+
// only (INV-02 still trivially holds: the binding is still exactly what a rebuild would produce).
|
|
138
|
+
await deps.bindingRepo.upsert({
|
|
139
|
+
workspaceId: input.workspaceId,
|
|
140
|
+
regionKey: currentPayload.regionKey,
|
|
141
|
+
areaEntryId: result.value.entry.id,
|
|
142
|
+
updatedAt: clockNowIso({ clock: deps.clock }),
|
|
143
|
+
});
|
|
144
|
+
return { areaEntry: toWidgetAreaEntry({ entry: result.value.entry }) };
|
|
145
|
+
} });
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* REQ-12/14: reconciles `widget_region_bindings` from the authoritative `widget_area` entries. The
|
|
149
|
+
* sole write path for the binding table (INV-02) — a full rescan-and-replace, exactly mirroring
|
|
150
|
+
* `navigation/reconcile.ts`'s `rebuildNavLocationBindings`. A region key present on more than one
|
|
151
|
+
* `widget_area` entry (a data-drift edge case the invariant should otherwise prevent) resolves
|
|
152
|
+
* last-writer-wins by iteration order, consistent with `WidgetRegionBindingRepoPort.upsert`'s own
|
|
153
|
+
* semantics elsewhere in this library.
|
|
154
|
+
*
|
|
155
|
+
* Orphan handling (REQ-14): this reconcile pass only ever WRITES rows implied by live `widget_area`
|
|
156
|
+
* entries — it never independently marks a row inactive for a region no theme declares anymore
|
|
157
|
+
* (that requires theme-declaration data this function is not given). `markInactive` is `widgets/
|
|
158
|
+
* repo.{memory,sqlite}.ts`'s own export for that call site (out of this task's scope — no HTTP/
|
|
159
|
+
* theme-activation route exists yet to drive it).
|
|
160
|
+
*
|
|
161
|
+
* @complexity O(n) over the workspace's `widget_area` entry count.
|
|
162
|
+
* @overallScore 100
|
|
163
|
+
*/
|
|
164
|
+
export async function reconcileWidgetRegionBindings(required, _optional = {}) {
|
|
165
|
+
const { deps, input } = required;
|
|
166
|
+
const areaEntries = await deps.entryRepo.listByWorkspace({ workspaceId: input.workspaceId }, { type: WIDGET_AREA_CONTENT_TYPE });
|
|
167
|
+
const now = clockNowIso({ clock: deps.clock });
|
|
168
|
+
const byRegion = new Map();
|
|
169
|
+
for (const entry of areaEntries) {
|
|
170
|
+
const payload = parseWidgetAreaPayload({ fieldsJson: entry.fieldsJson });
|
|
171
|
+
byRegion.set(payload.regionKey, {
|
|
172
|
+
workspaceId: input.workspaceId,
|
|
173
|
+
regionKey: payload.regionKey,
|
|
174
|
+
areaEntryId: entry.id,
|
|
175
|
+
updatedAt: now,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
await deps.bindingRepo.rebuildForWorkspace({ workspaceId: input.workspaceId, bindings: [...byRegion.values()] });
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=region-area-service.js.map
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The v1 widget-type registry (SPEC-043 REQ-07..10, ADR-047 Debate Fold-In Amendment 3).
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* Each `WidgetTypeRegistration` below is plain, JSON-serializable data — schema, capability
|
|
6
|
+
* class, placement contexts, cost clamps, and (for dynamic types) a `resolverId` string. No
|
|
7
|
+
* registration record imports or references executable behavior directly; `resolverId` is a
|
|
8
|
+
* pointer that only ever resolves through `resolvers/index.ts`'s closed `CORE_RESOLVERS` map
|
|
9
|
+
* (REQ-08) — never a dynamic import, `eval`, or arbitrary function reference. This is the split
|
|
10
|
+
* Amendment 3 requires: type *registration* is data (Tier-1-safe, this file); type *behavior* is
|
|
11
|
+
* core-owned code in v1, ADR-024 Tier-2/3-gated for any future plugin-contributed dynamic type
|
|
12
|
+
* (`resolvers/`).
|
|
13
|
+
*
|
|
14
|
+
* `getWidgetTypeRegistration`/`findWidgetTypeRegistration` are the two lookup accessors over this
|
|
15
|
+
* data — pure, O(1) functions with no I/O and no reference to resolver code, kept here (unlike
|
|
16
|
+
* `forms/manifest.ts`'s stricter zero-function convention, which exists specifically for a
|
|
17
|
+
* hypothetical future loader retrofit this file has no equivalent of) because every other registry
|
|
18
|
+
* in this codebase (`identity/permissions.ts`) pairs its data with a plain accessor the same way.
|
|
19
|
+
* The split is deliberate, not incidental: `getWidgetTypeRegistration` is total and only accepts a
|
|
20
|
+
* `typeKey` the type system has actually proven is one of the five registered keys; a caller
|
|
21
|
+
* holding a merely-asserted `WidgetTypeKey` (cast from an HTTP body or decoded JSON, not narrowed)
|
|
22
|
+
* must use the partial `findWidgetTypeRegistration` instead — see each function's own doc.
|
|
23
|
+
*
|
|
24
|
+
* How it relates to the project:
|
|
25
|
+
* - Read by `resolvers/index.ts` (dispatch), `write-service.ts` (config validation, REQ-02/03),
|
|
26
|
+
* and any admin/AI surface that needs to know what a widget type declares.
|
|
27
|
+
* - Static/type-only until the registry itself is imported and iterated — no side effects at
|
|
28
|
+
* module load time.
|
|
29
|
+
*/
|
|
30
|
+
import type { WidgetTypeKey, WidgetTypeRegistration } from "./types.js";
|
|
31
|
+
/**
|
|
32
|
+
* The complete v1 widget-type registry (REQ-09), keyed by the closed `WidgetTypeKey` union rather
|
|
33
|
+
* than held as an array. Declared with `satisfies` rather than a `: Readonly<Record<...>>`
|
|
34
|
+
* annotation — a `:` annotation would widen every entry back to the shared `WidgetTypeRegistration`
|
|
35
|
+
* interface (losing e.g. `RECENT_ENTRIES_REGISTRATION`'s own non-optional `clamps.maxItems`);
|
|
36
|
+
* `satisfies` checks the same shape constraint (every `WidgetTypeKey` has an entry, which is what
|
|
37
|
+
* makes `getWidgetTypeRegistration` below total) while keeping each entry's own literal type intact.
|
|
38
|
+
* Iterate with `Object.values(...)` where the whole table is needed (e.g. `agent-tools.ts`'s
|
|
39
|
+
* published-type-list).
|
|
40
|
+
*/
|
|
41
|
+
export declare const WIDGET_TYPE_REGISTRATIONS: {
|
|
42
|
+
text: {
|
|
43
|
+
typeKey: "text";
|
|
44
|
+
capability: "static";
|
|
45
|
+
configSchema: {
|
|
46
|
+
type: string;
|
|
47
|
+
properties: {
|
|
48
|
+
body: {
|
|
49
|
+
type: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
required: string[];
|
|
53
|
+
additionalProperties: false;
|
|
54
|
+
};
|
|
55
|
+
placementContexts: ("region" | "inline")[];
|
|
56
|
+
clamps: {
|
|
57
|
+
timeoutMs: number;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
"social-links": {
|
|
61
|
+
typeKey: "social-links";
|
|
62
|
+
capability: "static";
|
|
63
|
+
configSchema: {
|
|
64
|
+
type: string;
|
|
65
|
+
properties: {
|
|
66
|
+
links: {
|
|
67
|
+
type: string;
|
|
68
|
+
items: {
|
|
69
|
+
type: string;
|
|
70
|
+
properties: {
|
|
71
|
+
platform: {
|
|
72
|
+
type: string;
|
|
73
|
+
};
|
|
74
|
+
url: {
|
|
75
|
+
type: string;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
required: string[];
|
|
79
|
+
additionalProperties: false;
|
|
80
|
+
};
|
|
81
|
+
maxItems: number;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
required: string[];
|
|
85
|
+
additionalProperties: false;
|
|
86
|
+
};
|
|
87
|
+
placementContexts: ("region" | "inline")[];
|
|
88
|
+
clamps: {
|
|
89
|
+
timeoutMs: number;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
"recent-entries": {
|
|
93
|
+
typeKey: "recent-entries";
|
|
94
|
+
capability: "query";
|
|
95
|
+
configSchema: {
|
|
96
|
+
type: string;
|
|
97
|
+
properties: {
|
|
98
|
+
maxItems: {
|
|
99
|
+
type: string;
|
|
100
|
+
minimum: number;
|
|
101
|
+
maximum: number;
|
|
102
|
+
};
|
|
103
|
+
categoryTermId: {
|
|
104
|
+
type: string;
|
|
105
|
+
"x-ref-target": string;
|
|
106
|
+
};
|
|
107
|
+
collection: {
|
|
108
|
+
type: string;
|
|
109
|
+
};
|
|
110
|
+
where: {
|
|
111
|
+
type: string;
|
|
112
|
+
};
|
|
113
|
+
sort: {
|
|
114
|
+
type: string;
|
|
115
|
+
};
|
|
116
|
+
fields: {
|
|
117
|
+
type: string;
|
|
118
|
+
maxItems: number;
|
|
119
|
+
items: {
|
|
120
|
+
type: string;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
layout: {
|
|
124
|
+
type: string;
|
|
125
|
+
};
|
|
126
|
+
columns: {
|
|
127
|
+
type: string;
|
|
128
|
+
minimum: number;
|
|
129
|
+
maximum: number;
|
|
130
|
+
};
|
|
131
|
+
structuredData: {
|
|
132
|
+
type: string;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
required: string[];
|
|
136
|
+
additionalProperties: false;
|
|
137
|
+
};
|
|
138
|
+
placementContexts: ("region" | "inline")[];
|
|
139
|
+
clamps: {
|
|
140
|
+
maxItems: number;
|
|
141
|
+
timeoutMs: number;
|
|
142
|
+
};
|
|
143
|
+
resolverId: string;
|
|
144
|
+
};
|
|
145
|
+
menu: {
|
|
146
|
+
typeKey: "menu";
|
|
147
|
+
capability: "entry-reference";
|
|
148
|
+
configSchema: {
|
|
149
|
+
type: string;
|
|
150
|
+
properties: {
|
|
151
|
+
menuRef: {
|
|
152
|
+
type: string;
|
|
153
|
+
"x-ref-target": string;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
required: string[];
|
|
157
|
+
additionalProperties: false;
|
|
158
|
+
};
|
|
159
|
+
placementContexts: ("region" | "inline")[];
|
|
160
|
+
clamps: {
|
|
161
|
+
timeoutMs: number;
|
|
162
|
+
};
|
|
163
|
+
resolverId: string;
|
|
164
|
+
};
|
|
165
|
+
"contact-form": {
|
|
166
|
+
typeKey: "contact-form";
|
|
167
|
+
capability: "form";
|
|
168
|
+
configSchema: {
|
|
169
|
+
type: string;
|
|
170
|
+
properties: {
|
|
171
|
+
formDefinitionId: {
|
|
172
|
+
type: string;
|
|
173
|
+
"x-ref-target": string;
|
|
174
|
+
};
|
|
175
|
+
successMessage: {
|
|
176
|
+
type: string;
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
required: string[];
|
|
180
|
+
additionalProperties: false;
|
|
181
|
+
};
|
|
182
|
+
placementContexts: ("region" | "inline")[];
|
|
183
|
+
clamps: {
|
|
184
|
+
timeoutMs: number;
|
|
185
|
+
};
|
|
186
|
+
resolverId: string;
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* The total lookup accessor — pure, O(1), no I/O. Only valid for a `typeKey` already narrowed to
|
|
191
|
+
* `WidgetTypeKey` by the type system itself (a literal, or a value whose type was proven, not
|
|
192
|
+
* asserted). For a raw/untrusted string that merely claims to be a `WidgetTypeKey` (an HTTP body
|
|
193
|
+
* field cast at the boundary, e.g. `server/routes/admin/widgets/create.ts`, or a value decoded out
|
|
194
|
+
* of stored JSON via `entry-payload.ts`'s `parseWidgetInstancePayload`), `undefined` is genuinely
|
|
195
|
+
* reachable — use `findWidgetTypeRegistration` instead; do not smuggle an unverified string past
|
|
196
|
+
* this signature with a cast.
|
|
197
|
+
*
|
|
198
|
+
* Generic over the specific key, rather than fixed to the `WidgetTypeRegistration` interface, so a
|
|
199
|
+
* literal call site (`getWidgetTypeRegistration("recent-entries")`) gets back
|
|
200
|
+
* `WIDGET_TYPE_REGISTRATIONS`'s own entry type for that key — e.g. with `clamps.maxItems: number`,
|
|
201
|
+
* not the interface's shared `maxItems?: number` — instead of forcing an artificial fallback
|
|
202
|
+
* (`?? 20`) that coverage could never actually exercise for that one call site.
|
|
203
|
+
*/
|
|
204
|
+
export declare function getWidgetTypeRegistration<K extends WidgetTypeKey>({ typeKey }: {
|
|
205
|
+
typeKey: K;
|
|
206
|
+
}, _optional?: Record<string, never>): (typeof WIDGET_TYPE_REGISTRATIONS)[K];
|
|
207
|
+
/**
|
|
208
|
+
* The partial lookup accessor for a raw, untrusted string that has not been proven to be a
|
|
209
|
+
* `WidgetTypeKey` — only asserted to be one at some upstream boundary (an HTTP body field, or a
|
|
210
|
+
* value decoded out of stored JSON). Returns `undefined` for any string that isn't one of the five
|
|
211
|
+
* registered keys, which callers at those boundaries must handle for real (see
|
|
212
|
+
* `write-service.ts`'s `WidgetTypeUnregisteredError`, `resolvers/index.ts`'s `"unknown-type"`).
|
|
213
|
+
*/
|
|
214
|
+
export declare function findWidgetTypeRegistration({ raw }: {
|
|
215
|
+
raw: string;
|
|
216
|
+
}, _optional?: Record<string, never>): WidgetTypeRegistration | undefined;
|
|
217
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** `text` — static, no resolver (REQ-10). A single free-form rich-text field. */
|
|
2
|
+
const TEXT_REGISTRATION = {
|
|
3
|
+
typeKey: "text",
|
|
4
|
+
capability: "static",
|
|
5
|
+
configSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: { body: { type: "string" } },
|
|
8
|
+
required: ["body"],
|
|
9
|
+
additionalProperties: false,
|
|
10
|
+
},
|
|
11
|
+
placementContexts: ["region", "inline"],
|
|
12
|
+
clamps: { timeoutMs: 0 },
|
|
13
|
+
};
|
|
14
|
+
/** `social-links` — static, no resolver. An ordered list of `{ platform, url }` pairs. */
|
|
15
|
+
const SOCIAL_LINKS_REGISTRATION = {
|
|
16
|
+
typeKey: "social-links",
|
|
17
|
+
capability: "static",
|
|
18
|
+
configSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
links: {
|
|
22
|
+
type: "array",
|
|
23
|
+
items: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: { platform: { type: "string" }, url: { type: "string" } },
|
|
26
|
+
required: ["platform", "url"],
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
maxItems: 20,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ["links"],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
},
|
|
35
|
+
placementContexts: ["region", "inline"],
|
|
36
|
+
clamps: { timeoutMs: 0 },
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* `recent-entries` — dynamic, `query` capability. `maxItems` is clamped at the
|
|
40
|
+
* REGISTRATION level (the ceiling any instance's own config may request) AND
|
|
41
|
+
* re-enforced at the orchestration layer (REQ-25) — defense in depth, not
|
|
42
|
+
* redundant: this value is what a resolver *may* return at most; the
|
|
43
|
+
* orchestrator enforces it independent of the resolver's own discipline.
|
|
44
|
+
*
|
|
45
|
+
* Declared with `satisfies` rather than a `: WidgetTypeRegistration` annotation so this constant
|
|
46
|
+
* keeps its own literal shape — `clamps.maxItems: number`, not the shared interface's
|
|
47
|
+
* `maxItems?: number` — letting `getWidgetTypeRegistration("recent-entries")` (registry.ts) return
|
|
48
|
+
* a type that's honestly non-optional for THIS registration, without lying for the other four
|
|
49
|
+
* (`text`/`social-links`/`menu`/`contact-form`) that genuinely have no maxItems clamp.
|
|
50
|
+
*
|
|
51
|
+
* Collections plan R1 (2026-09-23): this widget becomes "Collection list" — it now accepts the
|
|
52
|
+
* same `collection`/`where`/`sort`/`fields`/`layout`/`columns` config keys the `{"type":"collection"}`
|
|
53
|
+
* marker does (`resolvers/recent-entries.ts` shares C1's `parseCollectionListConfig` parser for
|
|
54
|
+
* them). `maxItems` stays required and the type key stays `"recent-entries"` — no data migration —
|
|
55
|
+
* so every existing config (just `{maxItems}`, optionally `categoryTermId`) still validates and
|
|
56
|
+
* still means exactly what it meant before (D7): no `collection` set means "every user content
|
|
57
|
+
* type", `sort` defaults to `"updated"`. This write-time schema only checks shape (type/bounds);
|
|
58
|
+
* `parseCollectionListConfig` does the real field-name/value validation at resolve time.
|
|
59
|
+
*/
|
|
60
|
+
const RECENT_ENTRIES_REGISTRATION = {
|
|
61
|
+
typeKey: "recent-entries",
|
|
62
|
+
capability: "query",
|
|
63
|
+
configSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
maxItems: { type: "integer", minimum: 1, maximum: 20 },
|
|
67
|
+
// REQ-32/EC-03: a taxonomy-term-target soft reference, extracted into entry_refs with
|
|
68
|
+
// targetKind 'term' (no safe-delete guarantee) — see core/entry-refs/extractor.ts.
|
|
69
|
+
categoryTermId: { type: "string", "x-ref-target": "term" },
|
|
70
|
+
// Collections plan R1 additions — shape-only here; `parseCollectionListConfig` (shared with
|
|
71
|
+
// the `collection` marker, C1) validates field names/values against the named content type.
|
|
72
|
+
collection: { type: "string" },
|
|
73
|
+
where: { type: "object" },
|
|
74
|
+
sort: { type: "string" },
|
|
75
|
+
fields: { type: "array", maxItems: 20, items: { type: "string" } },
|
|
76
|
+
layout: { type: "string" },
|
|
77
|
+
columns: { type: "integer", minimum: 1, maximum: 6 },
|
|
78
|
+
// AW-7: opt-in schema.org block ("faq-page"); shape-only like `layout`, parsed in public-list.ts.
|
|
79
|
+
structuredData: { type: "string" },
|
|
80
|
+
},
|
|
81
|
+
required: ["maxItems"],
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
},
|
|
84
|
+
placementContexts: ["region", "inline"],
|
|
85
|
+
clamps: { maxItems: 20, timeoutMs: 500 },
|
|
86
|
+
resolverId: "recent-entries",
|
|
87
|
+
};
|
|
88
|
+
/** `menu` — dynamic, `entry-reference` capability. Delegates entirely to `navigation/resolver.ts`. */
|
|
89
|
+
const MENU_REGISTRATION = {
|
|
90
|
+
typeKey: "menu",
|
|
91
|
+
capability: "entry-reference",
|
|
92
|
+
configSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
// REQ-31: menuRef is an entry-target ref, extracted into entry_refs — see
|
|
95
|
+
// core/entry-refs/extractor.ts (matches the CONTACT_FORM_REGISTRATION 'x-ref-target' pattern
|
|
96
|
+
// below).
|
|
97
|
+
properties: { menuRef: { type: "string", "x-ref-target": "entry" } },
|
|
98
|
+
required: ["menuRef"],
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
},
|
|
101
|
+
placementContexts: ["region", "inline"],
|
|
102
|
+
clamps: { timeoutMs: 500 },
|
|
103
|
+
resolverId: "menu",
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* `contact-form` — dynamic, `form` capability. A thin adapter over `src/forms/`
|
|
107
|
+
* (REQ-36..39) — this registration carries no submission/rate-limit/mail
|
|
108
|
+
* config of its own; `formDefinitionId` is a ref-typed field extracted into
|
|
109
|
+
* `entry_refs` (REQ-31).
|
|
110
|
+
*/
|
|
111
|
+
const CONTACT_FORM_REGISTRATION = {
|
|
112
|
+
typeKey: "contact-form",
|
|
113
|
+
capability: "form",
|
|
114
|
+
configSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
formDefinitionId: { type: "string", "x-ref-target": "entry" },
|
|
118
|
+
successMessage: { type: "string" },
|
|
119
|
+
},
|
|
120
|
+
required: ["formDefinitionId"],
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
},
|
|
123
|
+
placementContexts: ["region", "inline"],
|
|
124
|
+
clamps: { timeoutMs: 500 },
|
|
125
|
+
resolverId: "contact-form",
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* The complete v1 widget-type registry (REQ-09), keyed by the closed `WidgetTypeKey` union rather
|
|
129
|
+
* than held as an array. Declared with `satisfies` rather than a `: Readonly<Record<...>>`
|
|
130
|
+
* annotation — a `:` annotation would widen every entry back to the shared `WidgetTypeRegistration`
|
|
131
|
+
* interface (losing e.g. `RECENT_ENTRIES_REGISTRATION`'s own non-optional `clamps.maxItems`);
|
|
132
|
+
* `satisfies` checks the same shape constraint (every `WidgetTypeKey` has an entry, which is what
|
|
133
|
+
* makes `getWidgetTypeRegistration` below total) while keeping each entry's own literal type intact.
|
|
134
|
+
* Iterate with `Object.values(...)` where the whole table is needed (e.g. `agent-tools.ts`'s
|
|
135
|
+
* published-type-list).
|
|
136
|
+
*/
|
|
137
|
+
export const WIDGET_TYPE_REGISTRATIONS = {
|
|
138
|
+
text: TEXT_REGISTRATION,
|
|
139
|
+
"social-links": SOCIAL_LINKS_REGISTRATION,
|
|
140
|
+
"recent-entries": RECENT_ENTRIES_REGISTRATION,
|
|
141
|
+
menu: MENU_REGISTRATION,
|
|
142
|
+
"contact-form": CONTACT_FORM_REGISTRATION,
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* The total lookup accessor — pure, O(1), no I/O. Only valid for a `typeKey` already narrowed to
|
|
146
|
+
* `WidgetTypeKey` by the type system itself (a literal, or a value whose type was proven, not
|
|
147
|
+
* asserted). For a raw/untrusted string that merely claims to be a `WidgetTypeKey` (an HTTP body
|
|
148
|
+
* field cast at the boundary, e.g. `server/routes/admin/widgets/create.ts`, or a value decoded out
|
|
149
|
+
* of stored JSON via `entry-payload.ts`'s `parseWidgetInstancePayload`), `undefined` is genuinely
|
|
150
|
+
* reachable — use `findWidgetTypeRegistration` instead; do not smuggle an unverified string past
|
|
151
|
+
* this signature with a cast.
|
|
152
|
+
*
|
|
153
|
+
* Generic over the specific key, rather than fixed to the `WidgetTypeRegistration` interface, so a
|
|
154
|
+
* literal call site (`getWidgetTypeRegistration("recent-entries")`) gets back
|
|
155
|
+
* `WIDGET_TYPE_REGISTRATIONS`'s own entry type for that key — e.g. with `clamps.maxItems: number`,
|
|
156
|
+
* not the interface's shared `maxItems?: number` — instead of forcing an artificial fallback
|
|
157
|
+
* (`?? 20`) that coverage could never actually exercise for that one call site.
|
|
158
|
+
*/
|
|
159
|
+
export function getWidgetTypeRegistration({ typeKey }, _optional = {}) {
|
|
160
|
+
return WIDGET_TYPE_REGISTRATIONS[typeKey];
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The partial lookup accessor for a raw, untrusted string that has not been proven to be a
|
|
164
|
+
* `WidgetTypeKey` — only asserted to be one at some upstream boundary (an HTTP body field, or a
|
|
165
|
+
* value decoded out of stored JSON). Returns `undefined` for any string that isn't one of the five
|
|
166
|
+
* registered keys, which callers at those boundaries must handle for real (see
|
|
167
|
+
* `write-service.ts`'s `WidgetTypeUnregisteredError`, `resolvers/index.ts`'s `"unknown-type"`).
|
|
168
|
+
*/
|
|
169
|
+
export function findWidgetTypeRegistration({ raw }, _optional = {}) {
|
|
170
|
+
return Object.values(WIDGET_TYPE_REGISTRATIONS).find((registration) => registration.typeKey === raw);
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { UUID } from "@jini-ai/core/primitives";
|
|
2
|
+
import type { WidgetRegionBindingRepoPort } from "./ports.js";
|
|
3
|
+
import type { WidgetRegionBindingRow, WidgetRegionKey } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* @file In-memory `WidgetRegionBindingRepoPort` adapter (ADR-006 rule-of-two "one being built now"
|
|
6
|
+
* half — `repo.sqlite.ts` is the other), mirroring `navigation/repo.memory.ts`'s
|
|
7
|
+
* `InMemoryNavLocationBindingRepo` exactly: enforces `UNIQUE(workspace_id, region_key)` by storing
|
|
8
|
+
* at most one row per `(workspaceId, regionKey)` pair, `upsert` replacing any prior row for that
|
|
9
|
+
* key (last-writer-wins, same discipline `NavLocationBindingRepoPort.upsert` documents).
|
|
10
|
+
*
|
|
11
|
+
* Architectural role:
|
|
12
|
+
* Infrastructure adapter (in-memory). No feature logic — that is `region-area-service.ts`'s job;
|
|
13
|
+
* this class is a dumb, uniqueness-enforcing collection.
|
|
14
|
+
*/
|
|
15
|
+
export declare class InMemoryWidgetRegionBindingRepo implements WidgetRegionBindingRepoPort {
|
|
16
|
+
private rows;
|
|
17
|
+
constructor(initialRows?: WidgetRegionBindingRow[]);
|
|
18
|
+
findByRegion(required: {
|
|
19
|
+
workspaceId: UUID;
|
|
20
|
+
regionKey: WidgetRegionKey;
|
|
21
|
+
}): Promise<WidgetRegionBindingRow | null>;
|
|
22
|
+
listByWorkspace(required: {
|
|
23
|
+
workspaceId: UUID;
|
|
24
|
+
}): Promise<WidgetRegionBindingRow[]>;
|
|
25
|
+
upsert(required: {
|
|
26
|
+
workspaceId: UUID;
|
|
27
|
+
regionKey: WidgetRegionKey;
|
|
28
|
+
areaEntryId: UUID;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
}): Promise<WidgetRegionBindingRow>;
|
|
31
|
+
/** REQ-14 — a theme switch orphaned this region key: drop the binding row, retain the underlying `widget_area` entry (untouched by this port). */
|
|
32
|
+
markInactive(required: {
|
|
33
|
+
workspaceId: UUID;
|
|
34
|
+
regionKey: WidgetRegionKey;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
rebuildForWorkspace(required: {
|
|
37
|
+
workspaceId: UUID;
|
|
38
|
+
bindings: readonly WidgetRegionBindingRow[];
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=repo.memory.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file In-memory `WidgetRegionBindingRepoPort` adapter (ADR-006 rule-of-two "one being built now"
|
|
3
|
+
* half — `repo.sqlite.ts` is the other), mirroring `navigation/repo.memory.ts`'s
|
|
4
|
+
* `InMemoryNavLocationBindingRepo` exactly: enforces `UNIQUE(workspace_id, region_key)` by storing
|
|
5
|
+
* at most one row per `(workspaceId, regionKey)` pair, `upsert` replacing any prior row for that
|
|
6
|
+
* key (last-writer-wins, same discipline `NavLocationBindingRepoPort.upsert` documents).
|
|
7
|
+
*
|
|
8
|
+
* Architectural role:
|
|
9
|
+
* Infrastructure adapter (in-memory). No feature logic — that is `region-area-service.ts`'s job;
|
|
10
|
+
* this class is a dumb, uniqueness-enforcing collection.
|
|
11
|
+
*/
|
|
12
|
+
export class InMemoryWidgetRegionBindingRepo {
|
|
13
|
+
rows;
|
|
14
|
+
constructor(initialRows = []) {
|
|
15
|
+
this.rows = [...initialRows];
|
|
16
|
+
}
|
|
17
|
+
async findByRegion(required) {
|
|
18
|
+
return (this.rows.find((row) => row.workspaceId === required.workspaceId && row.regionKey === required.regionKey) ?? null);
|
|
19
|
+
}
|
|
20
|
+
async listByWorkspace(required) {
|
|
21
|
+
return this.rows.filter((row) => row.workspaceId === required.workspaceId);
|
|
22
|
+
}
|
|
23
|
+
async upsert(required) {
|
|
24
|
+
const row = {
|
|
25
|
+
workspaceId: required.workspaceId,
|
|
26
|
+
regionKey: required.regionKey,
|
|
27
|
+
areaEntryId: required.areaEntryId,
|
|
28
|
+
updatedAt: required.updatedAt,
|
|
29
|
+
};
|
|
30
|
+
const index = this.rows.findIndex((existing) => existing.workspaceId === required.workspaceId && existing.regionKey === required.regionKey);
|
|
31
|
+
if (index === -1) {
|
|
32
|
+
this.rows.push(row);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.rows[index] = row;
|
|
36
|
+
}
|
|
37
|
+
return row;
|
|
38
|
+
}
|
|
39
|
+
/** REQ-14 — a theme switch orphaned this region key: drop the binding row, retain the underlying `widget_area` entry (untouched by this port). */
|
|
40
|
+
async markInactive(required) {
|
|
41
|
+
this.rows = this.rows.filter((row) => !(row.workspaceId === required.workspaceId && row.regionKey === required.regionKey));
|
|
42
|
+
}
|
|
43
|
+
async rebuildForWorkspace(required) {
|
|
44
|
+
const others = this.rows.filter((row) => row.workspaceId !== required.workspaceId);
|
|
45
|
+
this.rows = [...others, ...required.bindings];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=repo.memory.js.map
|