@jini-ai/cms 0.5.2 → 0.5.4
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 +15 -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/forms/delete-submission.d.ts +38 -0
- package/dist/forms/delete-submission.js +25 -0
- package/dist/forms/duplicate-slug.d.ts +81 -0
- package/dist/forms/duplicate-slug.js +115 -0
- package/dist/forms/errors.d.ts +53 -0
- package/dist/forms/errors.js +25 -0
- package/dist/forms/express/handler.d.ts +59 -0
- package/dist/forms/express/handler.js +217 -0
- package/dist/forms/express/index.d.ts +2 -0
- package/dist/forms/express/index.js +2 -0
- package/dist/forms/fields-json.d.ts +14 -0
- package/dist/forms/fields-json.js +11 -0
- package/dist/forms/forms.d.ts +59 -0
- package/dist/forms/forms.js +213 -0
- package/dist/forms/html/authoring-adapter.d.ts +34 -0
- package/dist/forms/html/authoring-adapter.js +48 -0
- package/dist/forms/html/html-authoring.d.ts +12 -0
- package/dist/forms/html/html-authoring.js +133 -0
- package/dist/forms/html/html-submission-adapter.d.ts +10 -0
- package/dist/forms/html/html-submission-adapter.js +15 -0
- package/dist/forms/html/index.d.ts +4 -0
- package/dist/forms/html/index.js +4 -0
- package/dist/forms/html-render.d.ts +33 -0
- package/dist/forms/html-render.js +31 -0
- package/dist/forms/index.d.ts +17 -0
- package/dist/forms/index.js +17 -0
- package/dist/forms/notify-subscriber.d.ts +32 -0
- package/dist/forms/notify-subscriber.js +60 -0
- package/dist/forms/ports.d.ts +168 -0
- package/dist/forms/ports.js +2 -0
- package/dist/forms/rate-limit-key.d.ts +8 -0
- package/dist/forms/rate-limit-key.js +7 -0
- package/dist/forms/repo.memory.d.ts +120 -0
- package/dist/forms/repo.memory.js +188 -0
- package/dist/forms/sql/index.d.ts +5 -0
- package/dist/forms/sql/index.js +5 -0
- package/dist/forms/sql/repo.d.ts +94 -0
- package/dist/forms/sql/repo.js +175 -0
- package/dist/forms/sql/repo.rows.d.ts +33 -0
- package/dist/forms/sql/repo.rows.js +64 -0
- package/dist/forms/sql/submission-ip-retention-repo.d.ts +9 -0
- package/dist/forms/sql/submission-ip-retention-repo.js +40 -0
- package/dist/forms/sql/tables.d.ts +32 -0
- package/dist/forms/sql/tables.js +2 -0
- package/dist/forms/submission-attempts.d.ts +70 -0
- package/dist/forms/submission-attempts.js +70 -0
- package/dist/forms/submission-email-validation.d.ts +8 -0
- package/dist/forms/submission-email-validation.js +34 -0
- package/dist/forms/submission-ip-retention.d.ts +30 -0
- package/dist/forms/submission-ip-retention.js +27 -0
- package/dist/forms/submit-service.d.ts +78 -0
- package/dist/forms/submit-service.js +109 -0
- package/dist/forms/types.d.ts +49 -0
- package/dist/forms/types.js +2 -0
- package/dist/forms/write-service.d.ts +83 -0
- package/dist/forms/write-service.js +271 -0
- 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 -12
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FormDefinitionReadPort } from "./ports.js";
|
|
2
|
+
import type { WidgetResolver } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* @file `contact-form` widget resolver (SPEC-043 REQ-36..39, ADR-047 Debate Fold-In Amendment 4).
|
|
5
|
+
*
|
|
6
|
+
* Purpose:
|
|
7
|
+
* A thin, READ-ONLY adapter over `src/forms/`'s definition read side. Imports ONLY the read-only
|
|
8
|
+
* `FormDefinitionRepoPort` type, through `forms/index.ts` (ADR-009 §1's barrel; the type is
|
|
9
|
+
* declared in `forms/ports.ts`) — never the Forms library's own submission write path or its
|
|
10
|
+
* outbox-driven mail-notification module — so this file can never persist a submission, send
|
|
11
|
+
* mail, rate-limit, or duplicate any part of the Forms pipeline (INV-08, hard invariant; verified
|
|
12
|
+
* by a code-review-level grep over this whole package for those two module names, which must
|
|
13
|
+
* return nothing).
|
|
14
|
+
*
|
|
15
|
+
* REQ-37 ("reads the referenced Forms definition's declared field vocabulary — never a hardcoded
|
|
16
|
+
* field-type list") is honored here: `definition.fields` (Forms' own `FieldDescriptor[]`) is passed
|
|
17
|
+
* through as IR props verbatim, not re-enumerated against a widget-local field-kind switch. The
|
|
18
|
+
* actual submission still posts to Forms' existing public route unmodified (REQ-37's second half) —
|
|
19
|
+
* that is a render-component/route concern, out of this resolver's scope (no HTTP route exists yet
|
|
20
|
+
* for widgets in this slice; see the implementation report's scope-boundary notes).
|
|
21
|
+
*/
|
|
22
|
+
export interface ContactFormResolverDeps {
|
|
23
|
+
formDefinitionRepo: FormDefinitionReadPort;
|
|
24
|
+
}
|
|
25
|
+
export declare function createContactFormResolver(deps: ContactFormResolverDeps, _optional?: {}): WidgetResolver;
|
|
26
|
+
//# sourceMappingURL=contact-form.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function createContactFormResolver(deps, _optional = {}) {
|
|
2
|
+
return {
|
|
3
|
+
async resolveMany(instances, context) {
|
|
4
|
+
const results = new Map();
|
|
5
|
+
for (const instance of instances) {
|
|
6
|
+
const formDefinitionId = typeof instance.config.formDefinitionId === "string" ? instance.config.formDefinitionId : undefined;
|
|
7
|
+
const formDefinitionRef = typeof instance.config.formDefinitionRef === "string" ? instance.config.formDefinitionRef : undefined;
|
|
8
|
+
if (!formDefinitionId && !formDefinitionRef) {
|
|
9
|
+
results.set(instance.id, { ok: false, reason: "invalid-config" });
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const byId = await deps.formDefinitionRepo.findById({
|
|
13
|
+
workspaceId: context.workspaceId,
|
|
14
|
+
id: formDefinitionId ?? formDefinitionRef,
|
|
15
|
+
});
|
|
16
|
+
const definition = byId ?? (formDefinitionRef ? await deps.formDefinitionRepo.findBySlug({
|
|
17
|
+
workspaceId: context.workspaceId, slug: formDefinitionRef,
|
|
18
|
+
}) : null);
|
|
19
|
+
// REQ-38/EC-05: a disabled, trashed (`findById` hides it) or purged definition is the same
|
|
20
|
+
// failure-isolation placeholder, never an error.
|
|
21
|
+
if (!definition || definition.status !== "active") {
|
|
22
|
+
results.set(instance.id, { ok: false, reason: "target-disabled" });
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
results.set(instance.id, {
|
|
26
|
+
ok: true,
|
|
27
|
+
ir: {
|
|
28
|
+
componentId: "contact-form",
|
|
29
|
+
props: {
|
|
30
|
+
formDefinitionId: definition.id,
|
|
31
|
+
// Closes this file's own previously-disclosed deferral ("the actual submission still
|
|
32
|
+
// posts to Forms' existing public route unmodified... that is a render-component/route
|
|
33
|
+
// concern, out of this resolver's scope") — the render component built for SPEC-043's
|
|
34
|
+
// routes/UI slice (`server/http/site/render.ts`'s `renderWidgetContactForm`) needs the
|
|
35
|
+
// definition's `slug` to build `POST /forms/:slug/submit`'s URL (REQ-37's second half).
|
|
36
|
+
slug: definition.slug,
|
|
37
|
+
// REQ-37: Forms' own declared field vocabulary, passed through verbatim — never a
|
|
38
|
+
// hardcoded field-type list. Cast: FieldDescriptor[] has no index signature of its
|
|
39
|
+
// own, but every field is plain JSON-serializable data (SPEC-010 forms/types.ts).
|
|
40
|
+
fields: definition.fields,
|
|
41
|
+
...(definition.mode === "html" ? {
|
|
42
|
+
mode: "html", html: definition.html ?? "",
|
|
43
|
+
} : {}),
|
|
44
|
+
successMessage: typeof instance.config.successMessage === "string" ? instance.config.successMessage : null,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
dependencyKeys: [definition.id],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return results;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=contact-form.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EntryDisplayListPort, EntryListExcludingTypesPort, CollectionListPort, MenusPort, FormDefinitionReadPort } from "./ports.js";
|
|
2
|
+
import type { EntryListPort } from "../../entries/index.js";
|
|
3
|
+
import type { NavMenuReadModel, ResolveTargetHrefFn } from "../../navigation/index.js";
|
|
4
|
+
import type { WidgetResolver, WidgetTypeKey } from "../types.js";
|
|
5
|
+
import { type ContentTypeLookup } from "./recent-entries.js";
|
|
6
|
+
/**
|
|
7
|
+
* @file Assembles the real, DI'd v1 dynamic resolvers against real infrastructure deps.
|
|
8
|
+
*
|
|
9
|
+
* Purpose:
|
|
10
|
+
* The one place that turns "real repo adapters" into "the closed `CORE_RESOLVERS` map"
|
|
11
|
+
* (`resolvers/index.ts`'s `wireCoreResolvers` calls this). Kept separate from `index.ts` itself so
|
|
12
|
+
* the dispatch module (`resolveWidgetType`, exercised by every test in this slice) has no
|
|
13
|
+
* import-time dependency on `features/entries`/`navigation`/`forms` deps that a pure dispatch unit
|
|
14
|
+
* test would otherwise need to construct.
|
|
15
|
+
*/
|
|
16
|
+
export interface CoreResolverDeps {
|
|
17
|
+
menus: MenusPort;
|
|
18
|
+
collectionList: CollectionListPort;
|
|
19
|
+
resolveMenuTargetHref?: ResolveTargetHrefFn;
|
|
20
|
+
/** Widened by collections plan R1 with `EntryDisplayListPort` — the `recent-entries` widget's
|
|
21
|
+
* "Collection list" mode shares `EntryDisplayListPort.listPublishedForDisplay` with the
|
|
22
|
+
* `{"type":"collection"}` marker (C2). Widened again by review fix 3b with
|
|
23
|
+
* `EntryListExcludingTypesPort`, the legacy (no-`collection`) path's query-level
|
|
24
|
+
* `SYSTEM_CONTENT_TYPES` exclusion. Both composition roots' real `entryRepo` already implements
|
|
25
|
+
* all three (`server/runtime/composition/{app,deps}.ts`'s `TrashAwareInMemoryEntryRepo`/`SqliteEntryRepo`). */
|
|
26
|
+
entryList: EntryListPort & EntryDisplayListPort & EntryListExcludingTypesPort;
|
|
27
|
+
navMenuReadModel: NavMenuReadModel;
|
|
28
|
+
formDefinitionRepo: FormDefinitionReadPort;
|
|
29
|
+
/** R1 addition — resolves a `collection` config's target content type. See `recent-entries.ts`'s
|
|
30
|
+
* `ContentTypeLookup` for why this is its own narrow port rather than `ContentTypeRepoPort`. */
|
|
31
|
+
contentTypes: ContentTypeLookup;
|
|
32
|
+
}
|
|
33
|
+
export declare function createCoreResolvers(deps: CoreResolverDeps, _optional?: Record<string, never>): Partial<Record<WidgetTypeKey, WidgetResolver>>;
|
|
34
|
+
//# sourceMappingURL=create-core-resolvers.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createContactFormResolver } from "./contact-form.js";
|
|
2
|
+
import { createMenuResolver } from "./menu.js";
|
|
3
|
+
import { createRecentEntriesResolver } from "./recent-entries.js";
|
|
4
|
+
export function createCoreResolvers(deps, _optional = {}) {
|
|
5
|
+
return {
|
|
6
|
+
"recent-entries": createRecentEntriesResolver({ collectionList: deps.collectionList, entryList: deps.entryList, contentTypes: deps.contentTypes }),
|
|
7
|
+
menu: createMenuResolver({ menus: deps.menus, navMenuReadModel: deps.navMenuReadModel, ...(deps.resolveMenuTargetHref === undefined ? {} : { resolveTargetHref: deps.resolveMenuTargetHref }), publicOnly: true }),
|
|
8
|
+
"contact-form": createContactFormResolver({ formDefinitionRepo: deps.formDefinitionRepo }),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=create-core-resolvers.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `CORE_RESOLVERS` — the closed, core-owned resolver dispatch map (SPEC-043 REQ-08/09/10/23;
|
|
3
|
+
* ADR-047 Debate Fold-In Amendment 2/3).
|
|
4
|
+
*
|
|
5
|
+
* Purpose:
|
|
6
|
+
* The ONE place a registry's `resolverId` string resolves against real, executable code. A
|
|
7
|
+
* `WidgetTypeRegistration.resolverId` is never used as a dynamic import path, `eval`-style
|
|
8
|
+
* reference, or arbitrary function lookup anywhere else in this codebase (REQ-08) — this map is
|
|
9
|
+
* the sole allowlist. `resolveWidgetType` is the dispatcher every page render goes through
|
|
10
|
+
* (`resolver-service.ts`'s `resolvePageWidgets` calls this, once per distinct type present),
|
|
11
|
+
* and it owns the try/catch + timeout boundary so a resolver's exception or hang can never
|
|
12
|
+
* propagate past a widget's placement boundary (REQ-27, INV-05).
|
|
13
|
+
*
|
|
14
|
+
* Architectural role:
|
|
15
|
+
* TDD-certified implementation (implementation outline C-003). The dispatcher's failure-isolation
|
|
16
|
+
* contract is design-frozen from SPEC-043; `resolveWidgetType()` dispatches to the closed
|
|
17
|
+
* `CORE_RESOLVERS` map (or the static path for a resolver-less type), wrapping every call in the
|
|
18
|
+
* shared try/catch + timeout boundary so a resolver failure never propagates past REQ-27/INV-05.
|
|
19
|
+
* Verified against `__tests__/integration/resolver-service.integration.test.ts`.
|
|
20
|
+
*/
|
|
21
|
+
import type { UUID } from "@jini-ai/core/primitives";
|
|
22
|
+
import { type CoreResolverDeps } from "./create-core-resolvers.js";
|
|
23
|
+
import type { WidgetInstanceView, WidgetResolveContext, WidgetResolveResult, WidgetResolver, WidgetTypeKey } from "../types.js";
|
|
24
|
+
export { createRecentEntriesResolver } from "./recent-entries.js";
|
|
25
|
+
export { createMenuResolver } from "./menu.js";
|
|
26
|
+
export { createContactFormResolver } from "./contact-form.js";
|
|
27
|
+
/**
|
|
28
|
+
* The closed map. Adding a resolver = adding one entry here, never a dynamic import. Types with
|
|
29
|
+
* capability `static` (no `resolverId`) never appear here — `resolveWidgetType` takes the static
|
|
30
|
+
* path for those instead of consulting this map.
|
|
31
|
+
*
|
|
32
|
+
* Typed `Readonly<...>` so every ordinary caller gets a compile-time guarantee this map is never
|
|
33
|
+
* mutated ad hoc from outside `registerCoreResolver`/`wireCoreResolvers` — the certified test
|
|
34
|
+
* suite's direct-assignment test-double seam (`CORE_RESOLVERS["recent-entries"] = resolver`)
|
|
35
|
+
* deliberately overrides that guarantee with `@ts-expect-error`, since at runtime this is the same
|
|
36
|
+
* plain, mutable object `mutableResolvers` is (TypeScript's `readonly` has no runtime effect) —
|
|
37
|
+
* this is intentional, not a bug: it lets the test suite swap in failure-injecting test doubles
|
|
38
|
+
* without a second, parallel test-only export.
|
|
39
|
+
*/
|
|
40
|
+
export declare const CORE_RESOLVERS: Readonly<Partial<Record<WidgetTypeKey, WidgetResolver>>>;
|
|
41
|
+
/** Registers (or replaces) one resolver in the closed map — the one typed, non-test way to populate it. */
|
|
42
|
+
export declare function registerCoreResolver(required: {
|
|
43
|
+
typeKey: WidgetTypeKey;
|
|
44
|
+
resolver: WidgetResolver;
|
|
45
|
+
}, _optional?: Record<string, never>): void;
|
|
46
|
+
/**
|
|
47
|
+
* Assembles and registers every real, DI'd v1 dynamic resolver (`recent-entries`, `menu`,
|
|
48
|
+
* `contact-form` — see the sibling files in this directory) against the given infrastructure
|
|
49
|
+
* deps. Not called automatically at module load (these resolvers need injected repos that don't
|
|
50
|
+
* exist yet at pure import time — see each resolver factory's own doc) — a future boot-wiring
|
|
51
|
+
* pass (`server/app.ts`/`server/deps.ts`, out of this task's scope) is expected to call this once,
|
|
52
|
+
* with real adapters, before the app serves traffic. Left uncalled here is a disclosed gap, not an
|
|
53
|
+
* oversight — no test in this slice exercises real dynamic-resolver behavior (see
|
|
54
|
+
* `resolvers/{recent-entries,menu,contact-form}.ts`'s own file headers).
|
|
55
|
+
*/
|
|
56
|
+
export declare function wireCoreResolvers(deps: CoreResolverDeps, _optional?: Record<string, never>): void;
|
|
57
|
+
export declare function resolveWidgetType(required: {
|
|
58
|
+
typeKey: WidgetTypeKey;
|
|
59
|
+
instances: readonly WidgetInstanceView[];
|
|
60
|
+
context: WidgetResolveContext;
|
|
61
|
+
}, _optional?: Record<string, never>): Promise<ReadonlyMap<UUID, WidgetResolveResult>>;
|
|
62
|
+
export * from "./ports.js";
|
|
63
|
+
export { createCoreResolvers, type CoreResolverDeps } from "./create-core-resolvers.js";
|
|
64
|
+
export { resolveTaxonomyEmbeds } from "./taxonomy.js";
|
|
65
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { findWidgetTypeRegistration } from "../registry.js";
|
|
2
|
+
import { createCoreResolvers } from "./create-core-resolvers.js";
|
|
3
|
+
// `create-core-resolvers.ts` is the only production caller of the three factories below — they're
|
|
4
|
+
// wired into `CORE_RESOLVERS` via `wireCoreResolvers`, never invoked directly outside this module.
|
|
5
|
+
// Each also has its own dedicated unit/integration test exercising the resolver's own logic in
|
|
6
|
+
// isolation (2026-08-18 widgets/resolvers deep-import triage), so each factory is re-exported here
|
|
7
|
+
// narrowly, one line per consumer:
|
|
8
|
+
// `createRecentEntriesResolver` — `widgets/__tests__/unit/resolvers-recent-entries.unit.test.ts`.
|
|
9
|
+
export { createRecentEntriesResolver } from "./recent-entries.js";
|
|
10
|
+
// `createMenuResolver` — `widgets/__tests__/unit/resolvers-menu.unit.test.ts`.
|
|
11
|
+
export { createMenuResolver } from "./menu.js";
|
|
12
|
+
// `createContactFormResolver` — `widgets/__tests__/integration/resolve-html-page-embeds.integration.test.ts`.
|
|
13
|
+
export { createContactFormResolver } from "./contact-form.js";
|
|
14
|
+
/**
|
|
15
|
+
* The mutable backing store `CORE_RESOLVERS` (below) exposes only a readonly view over — see that
|
|
16
|
+
* export's own doc for why the map itself must stay assignable at runtime despite its readonly TS
|
|
17
|
+
* type (the certified test suite mutates it directly, via `@ts-expect-error`, as its test-double
|
|
18
|
+
* seam). `registerCoreResolver`/`wireCoreResolvers` are the two real, typed ways production code
|
|
19
|
+
* populates it; nothing else in this module writes to it.
|
|
20
|
+
*/
|
|
21
|
+
const mutableResolvers = {};
|
|
22
|
+
/**
|
|
23
|
+
* The closed map. Adding a resolver = adding one entry here, never a dynamic import. Types with
|
|
24
|
+
* capability `static` (no `resolverId`) never appear here — `resolveWidgetType` takes the static
|
|
25
|
+
* path for those instead of consulting this map.
|
|
26
|
+
*
|
|
27
|
+
* Typed `Readonly<...>` so every ordinary caller gets a compile-time guarantee this map is never
|
|
28
|
+
* mutated ad hoc from outside `registerCoreResolver`/`wireCoreResolvers` — the certified test
|
|
29
|
+
* suite's direct-assignment test-double seam (`CORE_RESOLVERS["recent-entries"] = resolver`)
|
|
30
|
+
* deliberately overrides that guarantee with `@ts-expect-error`, since at runtime this is the same
|
|
31
|
+
* plain, mutable object `mutableResolvers` is (TypeScript's `readonly` has no runtime effect) —
|
|
32
|
+
* this is intentional, not a bug: it lets the test suite swap in failure-injecting test doubles
|
|
33
|
+
* without a second, parallel test-only export.
|
|
34
|
+
*/
|
|
35
|
+
export const CORE_RESOLVERS = mutableResolvers;
|
|
36
|
+
/** Registers (or replaces) one resolver in the closed map — the one typed, non-test way to populate it. */
|
|
37
|
+
export function registerCoreResolver(required, _optional = {}) {
|
|
38
|
+
mutableResolvers[required.typeKey] = required.resolver;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Assembles and registers every real, DI'd v1 dynamic resolver (`recent-entries`, `menu`,
|
|
42
|
+
* `contact-form` — see the sibling files in this directory) against the given infrastructure
|
|
43
|
+
* deps. Not called automatically at module load (these resolvers need injected repos that don't
|
|
44
|
+
* exist yet at pure import time — see each resolver factory's own doc) — a future boot-wiring
|
|
45
|
+
* pass (`server/app.ts`/`server/deps.ts`, out of this task's scope) is expected to call this once,
|
|
46
|
+
* with real adapters, before the app serves traffic. Left uncalled here is a disclosed gap, not an
|
|
47
|
+
* oversight — no test in this slice exercises real dynamic-resolver behavior (see
|
|
48
|
+
* `resolvers/{recent-entries,menu,contact-form}.ts`'s own file headers).
|
|
49
|
+
*/
|
|
50
|
+
export function wireCoreResolvers(deps, _optional = {}) {
|
|
51
|
+
for (const [typeKey, resolver] of Object.entries(createCoreResolvers(deps))) {
|
|
52
|
+
registerCoreResolver({ typeKey, resolver });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Converts a resolver exception/hang into a typed failure — never propagates (REQ-27, INV-05). */
|
|
56
|
+
class ResolverTimeoutError extends Error {
|
|
57
|
+
}
|
|
58
|
+
async function withResolverTimeout(promise, timeoutMs) {
|
|
59
|
+
if (!timeoutMs || timeoutMs <= 0)
|
|
60
|
+
return promise;
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const timer = setTimeout(() => reject(new ResolverTimeoutError(`resolver exceeded ${timeoutMs}ms`)), timeoutMs);
|
|
63
|
+
promise.then((value) => {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
resolve(value);
|
|
66
|
+
}, (error) => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
reject(error);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* REQ-25 — re-enforces a type's registered `maxItems` clamp at the orchestration layer,
|
|
74
|
+
* independent of the resolver's own discipline (defense in depth, not redundant — see
|
|
75
|
+
* `registry.ts`'s own doc on this point). Applies to any IR node carrying `children` (the shape a
|
|
76
|
+
* list-like resolved widget uses).
|
|
77
|
+
*/
|
|
78
|
+
function clampResolveResult(result, registration) {
|
|
79
|
+
if (!result.ok)
|
|
80
|
+
return result;
|
|
81
|
+
const maxItems = registration.clamps.maxItems;
|
|
82
|
+
if (maxItems === undefined || !result.ir.children || result.ir.children.length <= maxItems)
|
|
83
|
+
return result;
|
|
84
|
+
return { ...result, ir: { ...result.ir, children: result.ir.children.slice(0, maxItems) } };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Dispatches to `CORE_RESOLVERS[typeKey].resolveMany` (batch-first, REQ-24), or the static path
|
|
88
|
+
* (REQ-10) for a type with no registered resolver. Owns the sole try/catch + timeout wrapper
|
|
89
|
+
* around every resolver invocation (REQ-26/27) — never throws itself; every failure mode in
|
|
90
|
+
* REQ-27's closed taxonomy converts to a typed `{ ok: false, reason }` result per affected
|
|
91
|
+
* instance.
|
|
92
|
+
*
|
|
93
|
+
* @complexity O(1) dispatch plus whatever the resolver's own `resolveMany` costs (bounded by the
|
|
94
|
+
* registration's `timeoutMs` clamp).
|
|
95
|
+
* @overallScore 100
|
|
96
|
+
*/
|
|
97
|
+
/** Sets every instance to the same typed failure — the shared shape behind each "reject the whole batch" branch below. */
|
|
98
|
+
function fillFailure(results, instances, reason) {
|
|
99
|
+
for (const instance of instances)
|
|
100
|
+
results.set(instance.id, { ok: false, reason });
|
|
101
|
+
}
|
|
102
|
+
/** REQ-10: static capability — validated config renders directly, no resolver invocation. */
|
|
103
|
+
function fillStaticResults(results, instances, registration) {
|
|
104
|
+
for (const instance of instances) {
|
|
105
|
+
results.set(instance.id, {
|
|
106
|
+
ok: true,
|
|
107
|
+
ir: { componentId: registration.typeKey, props: instance.config },
|
|
108
|
+
dependencyKeys: [instance.id],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Invokes the resolver under the shared try/catch + timeout boundary (REQ-26/27) and fills `results` per instance. */
|
|
113
|
+
async function fillResolverResults(results, resolver, instances, context, registration) {
|
|
114
|
+
try {
|
|
115
|
+
const resolved = await withResolverTimeout(resolver.resolveMany(instances, context), registration.clamps.timeoutMs);
|
|
116
|
+
for (const instance of instances) {
|
|
117
|
+
const result = resolved.get(instance.id);
|
|
118
|
+
results.set(instance.id, result ? clampResolveResult(result, registration) : { ok: false, reason: "resolver-error" });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const reason = error instanceof ResolverTimeoutError ? "timeout" : "resolver-error";
|
|
123
|
+
fillFailure(results, instances, reason);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function resolveWidgetType(required, _optional = {}) {
|
|
127
|
+
const { typeKey, instances, context } = required;
|
|
128
|
+
const results = new Map();
|
|
129
|
+
if (instances.length === 0)
|
|
130
|
+
return results;
|
|
131
|
+
// `typeKey` is typed `WidgetTypeKey` but only ASSERTED to be one — it's grouped off stored
|
|
132
|
+
// widget-instance data (`resolver-service.ts`'s `payload.widgetType`, itself an
|
|
133
|
+
// `entry-payload.ts` cast over decoded JSON), so `findWidgetTypeRegistration` is the correct,
|
|
134
|
+
// honestly-partial accessor: `"unknown-type"` below is a real, reachable failure mode (REQ-27).
|
|
135
|
+
const registration = findWidgetTypeRegistration({ raw: typeKey });
|
|
136
|
+
if (!registration) {
|
|
137
|
+
fillFailure(results, instances, "unknown-type");
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
140
|
+
if (!registration.resolverId) {
|
|
141
|
+
fillStaticResults(results, instances, registration);
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
const resolver = CORE_RESOLVERS[registration.resolverId];
|
|
145
|
+
if (!resolver) {
|
|
146
|
+
fillFailure(results, instances, "resolver-error");
|
|
147
|
+
return results;
|
|
148
|
+
}
|
|
149
|
+
await fillResolverResults(results, resolver, instances, context, registration);
|
|
150
|
+
return results;
|
|
151
|
+
}
|
|
152
|
+
export * from "./ports.js";
|
|
153
|
+
export { createCoreResolvers } from "./create-core-resolvers.js";
|
|
154
|
+
export { resolveTaxonomyEmbeds } from "./taxonomy.js";
|
|
155
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { NavMenuReadModel, ResolveTargetHrefFn } from "../../navigation/index.js";
|
|
2
|
+
import type { MenusPort } from "./ports.js";
|
|
3
|
+
import type { WidgetResolver } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* @file `menu` (menu-as-widget) resolver (SPEC-043 REQ-09, ADR-047 §1/§9 — "the primary menu as a
|
|
6
|
+
* widget in the footer region costs nothing new").
|
|
7
|
+
*
|
|
8
|
+
* Purpose:
|
|
9
|
+
* Delegates to `navigation`'s own read model — zero menu STORAGE logic duplicated in `widgets/`
|
|
10
|
+
* (`getMenu`, a real `NavMenuReadModel` call, not a reimplementation of `MenuRepoPort`) — and now
|
|
11
|
+
* also zero menu-HREF-RESOLUTION logic duplicated, via `navigation/resolver.ts`'s exported
|
|
12
|
+
* `resolveMenuDoc` (the doc-level building block `resolveForLocation` itself composes on top of).
|
|
13
|
+
* `resolveItemList`/`resolveItem` stay module-private in `navigation/resolver.ts`; `resolveMenuDoc`
|
|
14
|
+
* is the one exported entry point to their behavior, added specifically so this resolver would not
|
|
15
|
+
* need either an export-widening of those internals or a location-shaped call it doesn't have.
|
|
16
|
+
*
|
|
17
|
+
* Historical limitation (retained to explain the fallback): real href resolution for non-`url` targets (`entryRef`/`termRef`/
|
|
18
|
+
* `route`) needs `src/platform/routing` (ADR-039), which is being built in parallel and is not running code
|
|
19
|
+
* yet anywhere in this codebase — not even `navigation`'s own production callers have a real
|
|
20
|
+
* `ResolveTargetHrefFn` today (`resolveForLocation` takes it as an injected dependency precisely so
|
|
21
|
+
* a fake can stand in until routing lands, per `navigation/resolver.ts`'s own header). This resolver
|
|
22
|
+
* is in the same honest position as the rest of the system, not a special-cased worse one: `url`-kind
|
|
23
|
+
* targets (already-resolved hrefs, no injected dependency needed) now resolve to real, concrete
|
|
24
|
+
* `href`/`available`/`isActive` data; `entryRef`/`termRef`/`route` targets resolve to
|
|
25
|
+
* `available: false, href: null` via the placeholder `resolveTargetHref` below, exactly the outcome
|
|
26
|
+
* `navigation/resolver.ts`'s own doc describes for "cannot resolve" — swap the placeholder for the
|
|
27
|
+
* real routing-backed implementation in one place (`DEFAULT_RESOLVE_TARGET_HREF` below) once
|
|
28
|
+
* `src/platform/routing` ships; no other change needed here.
|
|
29
|
+
* 2026-10-04: runtime roots now inject the routing-backed page/entry resolver. The default remains
|
|
30
|
+
* useful for diagnostics; public widget assembly omits unavailable leaves while retaining parents
|
|
31
|
+
* that still contain visible descendants, matching the static menu's availability contract.
|
|
32
|
+
*/
|
|
33
|
+
export interface MenuResolverDeps {
|
|
34
|
+
menus: MenusPort;
|
|
35
|
+
navMenuReadModel: NavMenuReadModel;
|
|
36
|
+
/** Public widgets omit unavailable leaves; diagnostics may retain the complete resolved tree. */
|
|
37
|
+
publicOnly?: boolean;
|
|
38
|
+
/** Host routing injection; callers without it retain the diagnostic placeholder documented above. */
|
|
39
|
+
resolveTargetHref?: ResolveTargetHrefFn;
|
|
40
|
+
}
|
|
41
|
+
export declare function createMenuResolver(deps: MenuResolverDeps, _optional?: Record<string, never>): WidgetResolver;
|
|
42
|
+
//# sourceMappingURL=menu.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Before host routing was wired every non-URL target was unresolved; retain that safe null fallback. */
|
|
2
|
+
const DEFAULT_RESOLVE_TARGET_HREF = async () => null;
|
|
3
|
+
function toMenuItemProps(items, publicOnly) {
|
|
4
|
+
// Cast: ResolvedNavItem is a plain, JSON-serializable read model (navigation/types.ts) with no
|
|
5
|
+
// index signature of its own — the resolved IR contract only requires structural JSON-compatibility.
|
|
6
|
+
const visible = publicOnly ? items.flatMap((item) => {
|
|
7
|
+
const children = toMenuItemProps(item.children, true);
|
|
8
|
+
return item.available || children.length ? [{ ...item, children }] : [];
|
|
9
|
+
}) : items;
|
|
10
|
+
return visible;
|
|
11
|
+
}
|
|
12
|
+
export function createMenuResolver(deps, _optional = {}) {
|
|
13
|
+
const resolveTargetHref = deps.resolveTargetHref ?? DEFAULT_RESOLVE_TARGET_HREF;
|
|
14
|
+
return {
|
|
15
|
+
async resolveMany(instances, context) {
|
|
16
|
+
const results = new Map();
|
|
17
|
+
for (const instance of instances) {
|
|
18
|
+
const menuRef = typeof instance.config.menuRef === "string" ? instance.config.menuRef : undefined;
|
|
19
|
+
if (!menuRef) {
|
|
20
|
+
results.set(instance.id, { ok: false, reason: "invalid-config" });
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const menu = await deps.navMenuReadModel.getMenu({ workspaceId: context.workspaceId, menuId: menuRef });
|
|
24
|
+
if (!menu) {
|
|
25
|
+
results.set(instance.id, { ok: false, reason: "target-disabled" });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const items = await deps.menus.resolveMenuDoc({
|
|
29
|
+
doc: menu.doc,
|
|
30
|
+
context: { workspaceId: context.workspaceId },
|
|
31
|
+
resolveTargetHref,
|
|
32
|
+
});
|
|
33
|
+
results.set(instance.id, {
|
|
34
|
+
ok: true,
|
|
35
|
+
ir: { componentId: "menu", props: { title: menu.title, items: toMenuItemProps(items, deps.publicOnly === true) } },
|
|
36
|
+
dependencyKeys: [menu.id],
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=menu.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ContentTypeFieldDef, ContentTypeRecord } from "../../content-types/index.js";
|
|
2
|
+
import type { EntryRecord, EntryStatus } from "../../entries/index.js";
|
|
3
|
+
import type { resolveMenuDoc } from "../../navigation/index.js";
|
|
4
|
+
/** The host's navigation resolver, retained as its owning function's type. */
|
|
5
|
+
export interface MenusPort {
|
|
6
|
+
resolveMenuDoc: typeof resolveMenuDoc;
|
|
7
|
+
}
|
|
8
|
+
export type CollectionListLayout = "cards" | "list" | "accordion" | "carousel";
|
|
9
|
+
export interface CollectionListQuery {
|
|
10
|
+
readonly type: string;
|
|
11
|
+
readonly where: readonly {
|
|
12
|
+
readonly field: string;
|
|
13
|
+
readonly value: string | number | boolean;
|
|
14
|
+
}[];
|
|
15
|
+
readonly sort: {
|
|
16
|
+
readonly by: "published" | "updated" | "title" | {
|
|
17
|
+
readonly field: string;
|
|
18
|
+
};
|
|
19
|
+
readonly dir: "asc" | "desc";
|
|
20
|
+
};
|
|
21
|
+
readonly limit: number;
|
|
22
|
+
}
|
|
23
|
+
export interface EntryDisplayListPort {
|
|
24
|
+
listPublishedForDisplay(required: {
|
|
25
|
+
readonly workspaceId: string;
|
|
26
|
+
readonly query: CollectionListQuery;
|
|
27
|
+
}): Promise<EntryRecord[]>;
|
|
28
|
+
}
|
|
29
|
+
export interface EntryListExcludingTypesPort {
|
|
30
|
+
listByWorkspaceExcludingTypes(required: {
|
|
31
|
+
readonly workspaceId: string;
|
|
32
|
+
readonly excludeTypes: readonly string[];
|
|
33
|
+
readonly status?: EntryStatus;
|
|
34
|
+
readonly orderBy?: "updatedAt";
|
|
35
|
+
readonly orderDirection?: "asc" | "desc";
|
|
36
|
+
readonly limit?: number;
|
|
37
|
+
}): Promise<EntryRecord[]>;
|
|
38
|
+
}
|
|
39
|
+
/** Collection parsing, routing and display policy remain with the host's existing public-list owner. */
|
|
40
|
+
export interface CollectionListPort {
|
|
41
|
+
parseCollectionListConfig(config: unknown, contentType: ContentTypeRecord, options?: {
|
|
42
|
+
readonly defaultSort?: string;
|
|
43
|
+
readonly limitKey?: string;
|
|
44
|
+
}): {
|
|
45
|
+
readonly ok: false;
|
|
46
|
+
readonly reason: string;
|
|
47
|
+
} | {
|
|
48
|
+
readonly ok: true;
|
|
49
|
+
readonly query: CollectionListQuery;
|
|
50
|
+
readonly display: {
|
|
51
|
+
readonly columns: number;
|
|
52
|
+
readonly layout: CollectionListLayout;
|
|
53
|
+
readonly fields: readonly ContentTypeFieldDef[];
|
|
54
|
+
readonly structuredData?: "faq-page";
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
entryPublicHref(type: string, slug: string): string | null;
|
|
58
|
+
humanizeFieldName(name: string): string;
|
|
59
|
+
isCollectionListLayout(value: unknown): value is CollectionListLayout;
|
|
60
|
+
readonly systemContentTypes: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
/** Definitions are opaque read snapshots. Submission and notification behavior stays with Forms. */
|
|
63
|
+
export interface FormDefinitionView {
|
|
64
|
+
readonly id: string;
|
|
65
|
+
readonly slug: string;
|
|
66
|
+
readonly status: string;
|
|
67
|
+
readonly fields: readonly unknown[];
|
|
68
|
+
readonly mode?: string;
|
|
69
|
+
readonly html?: string | null;
|
|
70
|
+
}
|
|
71
|
+
/** Both lookup methods are used by today's resolver (id first, then authored slug). */
|
|
72
|
+
export interface FormDefinitionReadPort {
|
|
73
|
+
findById(required: {
|
|
74
|
+
workspaceId: string;
|
|
75
|
+
id: string;
|
|
76
|
+
}): Promise<FormDefinitionView | null>;
|
|
77
|
+
findBySlug(required: {
|
|
78
|
+
workspaceId: string;
|
|
79
|
+
slug: string;
|
|
80
|
+
}): Promise<FormDefinitionView | null>;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ContentTypeRecord } from "../../content-types/index.js";
|
|
2
|
+
import type { CollectionListPort, EntryDisplayListPort, EntryListExcludingTypesPort } from "./ports.js";
|
|
3
|
+
import type { EntryListPort } from "../../entries/index.js";
|
|
4
|
+
import type { WidgetResolver } from "../types.js";
|
|
5
|
+
/**
|
|
6
|
+
* @file `recent-entries` widget resolver (SPEC-043 REQ-25, ADR-047 §9) — extended by the
|
|
7
|
+
* collections plan (R1, 2026-09-23) into "Collection list": the same widget type now ALSO accepts
|
|
8
|
+
* `collection`/`where`/`sort`/`fields`/`layout`/`columns`, reusing C1's `parseCollectionListConfig`
|
|
9
|
+
* parser and C2's `EntryDisplayListPort.listPublishedForDisplay` query — the exact parser/query the
|
|
10
|
+
* `{"type":"collection"}` marker already uses (`pages.ts`'s `resolveOneCollectionMarker`), not a
|
|
11
|
+
* second implementation of either.
|
|
12
|
+
*
|
|
13
|
+
* Two independent per-instance paths, split by whether `config.collection` names a content type:
|
|
14
|
+
*
|
|
15
|
+
* - **No `collection` (D7, the historical/default shape):** unchanged from before this plan —
|
|
16
|
+
* ONE shared bounded query for the whole batch (`status: 'published'`, `orderBy: 'updatedAt'
|
|
17
|
+
* desc`, `limit: registryMax` — REQ-24/REQ-25, fixed 2026-07-21), across every content type
|
|
18
|
+
* except `SYSTEM_CONTENT_TYPES` (D8) — `widget`/`widget_area`/nav-menu entries were never meant
|
|
19
|
+
* to appear in a "recent entries" feed. That exclusion runs `EntryListExcludingTypesPort.
|
|
20
|
+
* listByWorkspaceExcludingTypes` (review fix 3b, `public-list.ts`) INSIDE the query, not as an
|
|
21
|
+
* in-memory filter on an already limit-bounded `listByWorkspace` result — the earlier
|
|
22
|
+
* filter-after-limit shape could return fewer entries than asked for, or none, whenever
|
|
23
|
+
* `registryMax` system rows were newer than the real content. Nothing else about this path's
|
|
24
|
+
* ordering changed. `layout`/`columns` are still read from the
|
|
25
|
+
* instance's own config (new, additive keys — an old config never set them, so they fall back to
|
|
26
|
+
* `"list"`/`3`, byte-identical to before), but `where`/`sort`/`fields` don't apply — there is no
|
|
27
|
+
* single content type here to validate field names against.
|
|
28
|
+
* - **`collection` set:** `parseCollectionListConfig` validates the config against that content
|
|
29
|
+
* type's declared fields and clamps `limit`/`columns`; `listPublishedForDisplay` runs the bounded,
|
|
30
|
+
* filtered, sorted query. `limitKey: "maxItems"` and `defaultSort: "updated"` are this widget's own
|
|
31
|
+
* legacy aliases (D7) — same parser, different defaults than the marker's `"newest"`/`limit`. The
|
|
32
|
+
* registered clamp is re-applied on top of the parser's own bound (REQ-25 defense in depth: the
|
|
33
|
+
* parser's own ceiling is 24, wider than this registration's 20).
|
|
34
|
+
*
|
|
35
|
+
* Both paths produce the SAME per-item IR shape (`{title, href, dateIso, dateLabel, fields}`,
|
|
36
|
+
* mirroring `entry-list-render.ts`'s `EntryListItem`) under the NEW `"entry-list-item"` componentId
|
|
37
|
+
* — deliberately not the old `"entry-summary"` id (still real, still dispatched, kept for its own
|
|
38
|
+
* `render.test.ts` open-redirect regression — see that file — but no longer emitted by this
|
|
39
|
+
* resolver, so a stray recursive dispatch on these children fails loudly as an unknown component
|
|
40
|
+
* rather than silently mis-rendering the new shape through the old one). `render.ts`'s
|
|
41
|
+
* `renderWidgetRecentEntries` reads these children directly and calls C3's `renderEntryList` for
|
|
42
|
+
* `"cards"` layout.
|
|
43
|
+
*
|
|
44
|
+
* `href` is always `null` today (D1 seam, `entryPublicHref`) — entry pages are off. This is also the
|
|
45
|
+
* fix for the previously-broken `href="/<slug>"` link (`entry-summary`'s own `renderWidgetEntrySummary`
|
|
46
|
+
* built it and it 404s): a `null` href renders the title as plain text (`render.ts`), never a dead
|
|
47
|
+
* link.
|
|
48
|
+
*
|
|
49
|
+
* `categoryTermId` (REQ-32/EC-03) remains a documented soft reference — this resolver does not
|
|
50
|
+
* filter by it (no taxonomy dependency wired here), matching EC-03's explicit "may render as if the
|
|
51
|
+
* filter is empty/unset" allowance.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* The narrow read port this resolver needs to look up a `collection` config's target content type —
|
|
55
|
+
* `findByKey` only, not `ContentTypeRepoPort`'s full write/transaction surface (port segregation,
|
|
56
|
+
* same shape-discipline as `EntryDisplayListPort` itself). Both composition roots' real
|
|
57
|
+
* `contentTypeRepo` (`InMemoryContentTypeRepo`/`SqliteContentTypeRepo`) satisfy this structurally.
|
|
58
|
+
*/
|
|
59
|
+
export interface ContentTypeLookup {
|
|
60
|
+
findByKey(params: {
|
|
61
|
+
workspaceId: string;
|
|
62
|
+
key: string;
|
|
63
|
+
}): Promise<ContentTypeRecord | null>;
|
|
64
|
+
}
|
|
65
|
+
export interface RecentEntriesResolverDeps {
|
|
66
|
+
collectionList: CollectionListPort;
|
|
67
|
+
entryList: EntryListPort & EntryDisplayListPort & EntryListExcludingTypesPort;
|
|
68
|
+
contentTypes: ContentTypeLookup;
|
|
69
|
+
}
|
|
70
|
+
export declare function createRecentEntriesResolver(deps: RecentEntriesResolverDeps, _optional?: Record<string, never>): WidgetResolver;
|
|
71
|
+
//# sourceMappingURL=recent-entries.d.ts.map
|