@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.
Files changed (261) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +14 -8
  3. package/dist/comments/errors.d.ts +23 -0
  4. package/dist/comments/errors.js +25 -0
  5. package/dist/comments/express/index.d.ts +2 -0
  6. package/dist/comments/express/index.js +2 -0
  7. package/dist/comments/express/submit-handler.d.ts +30 -0
  8. package/dist/comments/express/submit-handler.js +85 -0
  9. package/dist/comments/hooks.d.ts +39 -0
  10. package/dist/comments/hooks.js +41 -0
  11. package/dist/comments/index.d.ts +11 -0
  12. package/dist/comments/index.js +11 -0
  13. package/dist/comments/ingress.d.ts +57 -0
  14. package/dist/comments/ingress.js +173 -0
  15. package/dist/comments/module.d.ts +94 -0
  16. package/dist/comments/module.js +73 -0
  17. package/dist/comments/ports.d.ts +184 -0
  18. package/dist/comments/ports.js +2 -0
  19. package/dist/comments/repo.memory.d.ts +70 -0
  20. package/dist/comments/repo.memory.js +120 -0
  21. package/dist/comments/sanitize.d.ts +15 -0
  22. package/dist/comments/sanitize.js +31 -0
  23. package/dist/comments/spam.heuristic.d.ts +8 -0
  24. package/dist/comments/spam.heuristic.js +54 -0
  25. package/dist/comments/sql/index.d.ts +3 -0
  26. package/dist/comments/sql/index.js +3 -0
  27. package/dist/comments/sql/repo.d.ts +77 -0
  28. package/dist/comments/sql/repo.js +190 -0
  29. package/dist/comments/sql/repo.rows.d.ts +49 -0
  30. package/dist/comments/sql/repo.rows.js +71 -0
  31. package/dist/comments/types.d.ts +187 -0
  32. package/dist/comments/types.js +39 -0
  33. package/dist/comments/write-service.d.ts +121 -0
  34. package/dist/comments/write-service.js +144 -0
  35. package/dist/core/commands/command.d.ts +2 -9
  36. package/dist/core/commands/command.js +2 -9
  37. package/dist/core/index.d.ts +2 -1
  38. package/dist/core/index.js +1 -0
  39. package/dist/core/keyset-cursor.d.ts +25 -0
  40. package/dist/core/keyset-cursor.js +20 -0
  41. package/dist/core/ports.d.ts +30 -0
  42. package/dist/core/ports.js +0 -1
  43. package/dist/core/tools/args.d.ts +1 -46
  44. package/dist/core/tools/args.js +1 -44
  45. package/dist/forms/delete-submission.d.ts +38 -0
  46. package/dist/forms/delete-submission.js +25 -0
  47. package/dist/forms/duplicate-slug.d.ts +81 -0
  48. package/dist/forms/duplicate-slug.js +115 -0
  49. package/dist/forms/errors.d.ts +53 -0
  50. package/dist/forms/errors.js +25 -0
  51. package/dist/forms/express/handler.d.ts +59 -0
  52. package/dist/forms/express/handler.js +217 -0
  53. package/dist/forms/express/index.d.ts +2 -0
  54. package/dist/forms/express/index.js +2 -0
  55. package/dist/forms/fields-json.d.ts +14 -0
  56. package/dist/forms/fields-json.js +11 -0
  57. package/dist/forms/forms.d.ts +59 -0
  58. package/dist/forms/forms.js +213 -0
  59. package/dist/forms/html/authoring-adapter.d.ts +34 -0
  60. package/dist/forms/html/authoring-adapter.js +48 -0
  61. package/dist/forms/html/html-authoring.d.ts +12 -0
  62. package/dist/forms/html/html-authoring.js +133 -0
  63. package/dist/forms/html/html-submission-adapter.d.ts +10 -0
  64. package/dist/forms/html/html-submission-adapter.js +15 -0
  65. package/dist/forms/html/index.d.ts +4 -0
  66. package/dist/forms/html/index.js +4 -0
  67. package/dist/forms/html-render.d.ts +33 -0
  68. package/dist/forms/html-render.js +31 -0
  69. package/dist/forms/index.d.ts +17 -0
  70. package/dist/forms/index.js +17 -0
  71. package/dist/forms/notify-subscriber.d.ts +32 -0
  72. package/dist/forms/notify-subscriber.js +60 -0
  73. package/dist/forms/ports.d.ts +168 -0
  74. package/dist/forms/ports.js +2 -0
  75. package/dist/forms/rate-limit-key.d.ts +8 -0
  76. package/dist/forms/rate-limit-key.js +7 -0
  77. package/dist/forms/repo.memory.d.ts +120 -0
  78. package/dist/forms/repo.memory.js +188 -0
  79. package/dist/forms/sql/index.d.ts +5 -0
  80. package/dist/forms/sql/index.js +5 -0
  81. package/dist/forms/sql/repo.d.ts +94 -0
  82. package/dist/forms/sql/repo.js +175 -0
  83. package/dist/forms/sql/repo.rows.d.ts +33 -0
  84. package/dist/forms/sql/repo.rows.js +64 -0
  85. package/dist/forms/sql/submission-ip-retention-repo.d.ts +9 -0
  86. package/dist/forms/sql/submission-ip-retention-repo.js +40 -0
  87. package/dist/forms/sql/tables.d.ts +32 -0
  88. package/dist/forms/sql/tables.js +2 -0
  89. package/dist/forms/submission-attempts.d.ts +70 -0
  90. package/dist/forms/submission-attempts.js +70 -0
  91. package/dist/forms/submission-email-validation.d.ts +8 -0
  92. package/dist/forms/submission-email-validation.js +34 -0
  93. package/dist/forms/submission-ip-retention.d.ts +30 -0
  94. package/dist/forms/submission-ip-retention.js +27 -0
  95. package/dist/forms/submit-service.d.ts +78 -0
  96. package/dist/forms/submit-service.js +109 -0
  97. package/dist/forms/types.d.ts +49 -0
  98. package/dist/forms/types.js +2 -0
  99. package/dist/forms/write-service.d.ts +83 -0
  100. package/dist/forms/write-service.js +271 -0
  101. package/dist/media/index.d.ts +0 -3
  102. package/dist/media/index.js +1 -4
  103. package/dist/media/node.d.ts +5 -0
  104. package/dist/media/node.js +6 -0
  105. package/dist/redirects/capture.d.ts +80 -0
  106. package/dist/redirects/capture.js +66 -0
  107. package/dist/redirects/express/index.d.ts +27 -0
  108. package/dist/redirects/express/index.js +40 -0
  109. package/dist/redirects/hit-sink.d.ts +56 -0
  110. package/dist/redirects/hit-sink.js +44 -0
  111. package/dist/redirects/host-config/index.d.ts +40 -0
  112. package/dist/redirects/host-config/index.js +145 -0
  113. package/dist/redirects/index.d.ts +14 -0
  114. package/dist/redirects/index.js +14 -0
  115. package/dist/redirects/matcher.d.ts +58 -0
  116. package/dist/redirects/matcher.js +144 -0
  117. package/dist/redirects/order.d.ts +30 -0
  118. package/dist/redirects/order.js +26 -0
  119. package/dist/redirects/ports.d.ts +242 -0
  120. package/dist/redirects/ports.internal.d.ts +57 -0
  121. package/dist/redirects/ports.internal.js +14 -0
  122. package/dist/redirects/ports.js +2 -0
  123. package/dist/redirects/redirects.d.ts +226 -0
  124. package/dist/redirects/redirects.js +590 -0
  125. package/dist/redirects/referrer-alias.d.ts +31 -0
  126. package/dist/redirects/referrer-alias.js +33 -0
  127. package/dist/redirects/repo.memory.d.ts +51 -0
  128. package/dist/redirects/repo.memory.js +98 -0
  129. package/dist/redirects/reserved-destination.d.ts +58 -0
  130. package/dist/redirects/reserved-destination.js +59 -0
  131. package/dist/redirects/resolver.d.ts +100 -0
  132. package/dist/redirects/resolver.js +87 -0
  133. package/dist/redirects/spa/index.d.ts +30 -0
  134. package/dist/redirects/spa/index.js +27 -0
  135. package/dist/redirects/sql/index.d.ts +4 -0
  136. package/dist/redirects/sql/index.js +4 -0
  137. package/dist/redirects/sql/repo.d.ts +82 -0
  138. package/dist/redirects/sql/repo.js +143 -0
  139. package/dist/redirects/sql/repo.rows.d.ts +49 -0
  140. package/dist/redirects/sql/repo.rows.js +79 -0
  141. package/dist/redirects/sql/row-types.d.ts +34 -0
  142. package/dist/redirects/sql/row-types.js +2 -0
  143. package/dist/redirects/target-gate.d.ts +30 -0
  144. package/dist/redirects/target-gate.js +85 -0
  145. package/dist/redirects/types.d.ts +213 -0
  146. package/dist/redirects/types.js +42 -0
  147. package/dist/seo/absolute-url.d.ts +58 -0
  148. package/dist/seo/absolute-url.js +117 -0
  149. package/dist/seo/dom/index.d.ts +14 -0
  150. package/dist/seo/dom/index.js +52 -0
  151. package/dist/seo/errors.d.ts +35 -0
  152. package/dist/seo/errors.js +35 -0
  153. package/dist/seo/feed.d.ts +49 -0
  154. package/dist/seo/feed.js +61 -0
  155. package/dist/seo/index.d.ts +13 -0
  156. package/dist/seo/index.js +13 -0
  157. package/dist/seo/media.d.ts +57 -0
  158. package/dist/seo/media.js +134 -0
  159. package/dist/seo/page-head.d.ts +18 -0
  160. package/dist/seo/page-head.js +114 -0
  161. package/dist/seo/ports.d.ts +221 -0
  162. package/dist/seo/ports.js +27 -0
  163. package/dist/seo/seo.d.ts +73 -0
  164. package/dist/seo/seo.js +230 -0
  165. package/dist/seo/sitemap.d.ts +139 -0
  166. package/dist/seo/sitemap.js +217 -0
  167. package/dist/seo/types.d.ts +278 -0
  168. package/dist/seo/types.js +2 -0
  169. package/dist/seo/write-service.d.ts +79 -0
  170. package/dist/seo/write-service.js +273 -0
  171. package/dist/seo/xml-escape.d.ts +16 -0
  172. package/dist/seo/xml-escape.js +19 -0
  173. package/dist/settings/express/cms-adapter.d.ts +20 -0
  174. package/dist/settings/express/cms-adapter.js +124 -0
  175. package/dist/settings/express/contracts.d.ts +135 -0
  176. package/dist/settings/express/contracts.js +2 -0
  177. package/dist/settings/express/http.d.ts +10 -0
  178. package/dist/settings/express/http.js +211 -0
  179. package/dist/settings/express/index.d.ts +5 -0
  180. package/dist/settings/express/index.js +5 -0
  181. package/dist/settings/express/shared.d.ts +68 -0
  182. package/dist/settings/express/shared.js +54 -0
  183. package/dist/settings/express/stream.d.ts +24 -0
  184. package/dist/settings/express/stream.js +147 -0
  185. package/dist/settings/index.d.ts +3 -38
  186. package/dist/settings/index.js +3 -35
  187. package/dist/taxonomy/write-service.d.ts +2 -30
  188. package/dist/widgets/authorize-helper.d.ts +42 -0
  189. package/dist/widgets/authorize-helper.js +16 -0
  190. package/dist/widgets/concurrency.d.ts +12 -0
  191. package/dist/widgets/concurrency.js +44 -0
  192. package/dist/widgets/config-validation.d.ts +34 -0
  193. package/dist/widgets/config-validation.js +125 -0
  194. package/dist/widgets/embed-service.d.ts +133 -0
  195. package/dist/widgets/embed-service.js +460 -0
  196. package/dist/widgets/embed-validation.d.ts +23 -0
  197. package/dist/widgets/embed-validation.js +38 -0
  198. package/dist/widgets/entry-payload.d.ts +103 -0
  199. package/dist/widgets/entry-payload.js +160 -0
  200. package/dist/widgets/errors.d.ts +136 -0
  201. package/dist/widgets/errors.js +136 -0
  202. package/dist/widgets/host-ports.d.ts +149 -0
  203. package/dist/widgets/host-ports.js +2 -0
  204. package/dist/widgets/html/html-embeds.d.ts +247 -0
  205. package/dist/widgets/html/html-embeds.js +408 -0
  206. package/dist/widgets/html/index.d.ts +5 -0
  207. package/dist/widgets/html/index.js +4 -0
  208. package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
  209. package/dist/widgets/html/legacy-widget-markers.js +45 -0
  210. package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
  211. package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
  212. package/dist/widgets/index.d.ts +20 -0
  213. package/dist/widgets/index.js +19 -0
  214. package/dist/widgets/markers/html-attributes.d.ts +98 -0
  215. package/dist/widgets/markers/html-attributes.js +216 -0
  216. package/dist/widgets/markers/index.d.ts +3 -0
  217. package/dist/widgets/markers/index.js +3 -0
  218. package/dist/widgets/markers/marker.d.ts +496 -0
  219. package/dist/widgets/markers/marker.js +780 -0
  220. package/dist/widgets/page-embed-types.d.ts +42 -0
  221. package/dist/widgets/page-embed-types.js +42 -0
  222. package/dist/widgets/ports.d.ts +142 -0
  223. package/dist/widgets/ports.js +2 -0
  224. package/dist/widgets/read-service.d.ts +81 -0
  225. package/dist/widgets/read-service.js +79 -0
  226. package/dist/widgets/region-area-service.d.ts +110 -0
  227. package/dist/widgets/region-area-service.js +180 -0
  228. package/dist/widgets/registry.d.ts +217 -0
  229. package/dist/widgets/registry.js +172 -0
  230. package/dist/widgets/repo.memory.d.ts +41 -0
  231. package/dist/widgets/repo.memory.js +48 -0
  232. package/dist/widgets/resolver-service.d.ts +209 -0
  233. package/dist/widgets/resolver-service.js +909 -0
  234. package/dist/widgets/resolvers/contact-form.d.ts +26 -0
  235. package/dist/widgets/resolvers/contact-form.js +54 -0
  236. package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
  237. package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
  238. package/dist/widgets/resolvers/index.d.ts +65 -0
  239. package/dist/widgets/resolvers/index.js +155 -0
  240. package/dist/widgets/resolvers/menu.d.ts +42 -0
  241. package/dist/widgets/resolvers/menu.js +43 -0
  242. package/dist/widgets/resolvers/ports.d.ts +82 -0
  243. package/dist/widgets/resolvers/ports.js +2 -0
  244. package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
  245. package/dist/widgets/resolvers/recent-entries.js +165 -0
  246. package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
  247. package/dist/widgets/resolvers/taxonomy.js +34 -0
  248. package/dist/widgets/sql/index.d.ts +3 -0
  249. package/dist/widgets/sql/index.js +3 -0
  250. package/dist/widgets/sql/repo.d.ts +41 -0
  251. package/dist/widgets/sql/repo.js +82 -0
  252. package/dist/widgets/sql/tables.d.ts +13 -0
  253. package/dist/widgets/sql/tables.js +2 -0
  254. package/dist/widgets/types.d.ts +213 -0
  255. package/dist/widgets/types.js +17 -0
  256. package/dist/widgets/where-used.d.ts +41 -0
  257. package/dist/widgets/where-used.js +11 -0
  258. package/dist/widgets/write-service.d.ts +204 -0
  259. package/dist/widgets/write-service.js +332 -0
  260. package/dist/workspace/delete.d.ts +1 -1
  261. package/package.json +220 -12
@@ -0,0 +1,60 @@
1
+ // Domain behavior owns its event contract; reading a host manifest back would invert dependency flow.
2
+ const FORM_SUBMISSION_RECEIVED_TOPIC = "form.submission.received";
3
+ export async function registerFormNotifySubscriber(deps, _optional = {}) {
4
+ return deps.bus.subscribe({ eventName: FORM_SUBMISSION_RECEIVED_TOPIC,
5
+ handler: async (event) => {
6
+ try {
7
+ const { workspaceId, formDefinitionId, submissionId } = event.payload;
8
+ const definition = await deps.formDefinitionRepo.findById({ workspaceId, id: formDefinitionId });
9
+ if (!definition || !definition.notify.enabled || definition.notify.recipients.length === 0) {
10
+ return;
11
+ }
12
+ const submission = await deps.formSubmissionRepo.findById({ workspaceId, id: submissionId });
13
+ if (!submission)
14
+ return;
15
+ for (const recipient of definition.notify.recipients) {
16
+ try {
17
+ const result = await deps.mailer.send({
18
+ message: {
19
+ workspaceId,
20
+ to: { email: recipient },
21
+ from: deps.sender,
22
+ subject: `New submission: ${definition.name}`,
23
+ text: JSON.stringify(submission.data),
24
+ },
25
+ options: {
26
+ // Include the recipient so a shared dedup ledger never suppresses a sibling send.
27
+ idempotencyKey: `forms:notify:${submissionId}:${recipient}`,
28
+ workspaceId,
29
+ sourceContext: { module: "forms", ref: formDefinitionId },
30
+ purpose: "transactional",
31
+ // Background notifications use the host mailer's durable-outbox readiness gate,
32
+ // rather than the interactive send lane, when production delivery requires it.
33
+ lane: "notification",
34
+ }
35
+ });
36
+ if (!result.ok) {
37
+ warn(deps.logger, `[forms:notify] mail send failed for submission '${submissionId}' recipient '${recipient}': ${result.errorCode} (${result.message})`);
38
+ }
39
+ }
40
+ catch (err) {
41
+ // A send failure is terminal for this delivery: log, never retry inline or alter
42
+ // the persisted submission, and continue so the other recipients can still receive it.
43
+ warn(deps.logger, `[forms:notify] mail send threw for submission '${submissionId}' recipient '${recipient}':`, err);
44
+ }
45
+ }
46
+ }
47
+ catch (err) {
48
+ // Repository failures must not escape the bus's subscriber loop and break sibling handlers.
49
+ warn(deps.logger, "[forms:notify] subscriber failed:", err);
50
+ }
51
+ } });
52
+ }
53
+ /** Failure reporting is isolated too: a logger cannot interrupt sibling event subscribers. */
54
+ function warn(logger, message, error) {
55
+ try {
56
+ logger.warn({ message }, error === undefined ? {} : { error });
57
+ }
58
+ catch { /* Host logging failure is terminal. */ }
59
+ }
60
+ //# sourceMappingURL=notify-subscriber.js.map
@@ -0,0 +1,168 @@
1
+ /** Forms persistence and outbound effect contracts. Concrete repositories, HTTP routing, tool wiring and transport implementations belong to consumers. */
2
+ import type { UUID } from "@jini-ai/core/primitives";
3
+ import type { FormDefinitionRecord, FormSubmissionPage, FormSubmissionRecord } from "./types.js";
4
+ /** Definitions and submissions have distinct lifecycles and therefore separate ports.
5
+ * Deletion belongs to the host's Trash/purge workflow, so neither persistence port exposes delete.
6
+ * Reads must hide trashed rows at the repository boundary: public submitters, widget resolvers
7
+ * and other direct consumers must get not-found even if they forget an additional domain filter. */
8
+ export interface FormDefinitionRepoPort {
9
+ /** Reads and updates expose only live records; a trashed row still reserves its slug. */
10
+ findById(required: {
11
+ workspaceId: UUID;
12
+ id: UUID;
13
+ }): Promise<FormDefinitionRecord | null>;
14
+ findBySlug(required: {
15
+ workspaceId: UUID;
16
+ slug: string;
17
+ }): Promise<FormDefinitionRecord | null>;
18
+ list(required: {
19
+ workspaceId: UUID;
20
+ }): Promise<FormDefinitionRecord[]>;
21
+ /** The database unique index is the tie-break for (workspaceId, slug); callers translate collisions.
22
+ * A trashed row retains its slug so restoring it is lossless, and can therefore still conflict. */
23
+ create(record: FormDefinitionRecord): Promise<void>;
24
+ /** Update only a live row, affecting zero rows if it was trashed meanwhile. Only Trash restore
25
+ * may revive it. Keep create/update separate so genuine insert uniqueness races remain visible. */
26
+ update(record: FormDefinitionRecord): Promise<void>;
27
+ /** Deliberately trash-blind: both live and trashed rows reserve a slug. Duplication must not
28
+ * propose a trashed slug as available and then collide with the database unique index. */
29
+ isSlugTaken(required: {
30
+ workspaceId: UUID;
31
+ slug: string;
32
+ }): Promise<boolean>;
33
+ }
34
+ export interface FormSubmissionRepoPort {
35
+ findById(required: {
36
+ workspaceId: UUID;
37
+ id: UUID;
38
+ }): Promise<FormSubmissionRecord | null>;
39
+ create(record: FormSubmissionRecord): Promise<void>;
40
+ /** Optional repository-owned transaction. Hosts may instead bind a wider unit of work. */
41
+ transaction?: SubmissionTransactionPort;
42
+ /** Newest first, submittedAt descending then id descending for ties. The opaque cursor resumes
43
+ * after the last returned row; callers validate limit (1..100) before reaching persistence. */
44
+ listByDefinition(required: {
45
+ workspaceId: UUID;
46
+ formDefinitionId: UUID;
47
+ limit: number;
48
+ }, optional?: {
49
+ cursor?: string | null;
50
+ }): Promise<FormSubmissionPage>;
51
+ }
52
+ export interface RateLimiterPort {
53
+ check(required: {
54
+ key: string;
55
+ }): {
56
+ allowed: true;
57
+ } | {
58
+ allowed: false;
59
+ retryAfterSeconds: number;
60
+ } | Promise<{
61
+ allowed: true;
62
+ } | {
63
+ allowed: false;
64
+ retryAfterSeconds: number;
65
+ }>;
66
+ }
67
+ export interface FormOutboxPort {
68
+ enqueue(event: import("../core/index.js").DomainEvent): Promise<void>;
69
+ }
70
+ /**
71
+ * Runs `work` as one unit of persistence: commit when it resolves, roll back when it throws. A host
72
+ * binds its database transaction here so an accepted submission's row and its outbox event are
73
+ * written together; a submission stored without its event would never notify anyone.
74
+ */
75
+ export type SubmissionTransactionPort = <T>(work: () => Promise<T>) => Promise<T>;
76
+ /** Bound to the host's outbox/bus/clock; dispatch is kicked off without awaiting delivery. */
77
+ export interface OutboxDispatcherPort {
78
+ dispatch(required: Record<string, never>): Promise<void>;
79
+ }
80
+ export interface EmailAddress {
81
+ email: string;
82
+ name?: string;
83
+ }
84
+ export interface OutboundEmail {
85
+ workspaceId: UUID;
86
+ to: EmailAddress;
87
+ from: EmailAddress;
88
+ subject: string;
89
+ text: string;
90
+ }
91
+ export interface MailerSendOptions {
92
+ idempotencyKey: string;
93
+ workspaceId: UUID;
94
+ sourceContext: {
95
+ module: string;
96
+ ref: string;
97
+ };
98
+ purpose: "transactional";
99
+ lane: "notification";
100
+ }
101
+ export type MailerSendResult = {
102
+ ok: true;
103
+ providerMessageId: string;
104
+ acceptedAt: string;
105
+ } | {
106
+ ok: false;
107
+ retryable: boolean;
108
+ errorCode: string;
109
+ message: string;
110
+ };
111
+ export interface MailerPort {
112
+ send(required: {
113
+ message: OutboundEmail;
114
+ options: MailerSendOptions;
115
+ }): Promise<MailerSendResult>;
116
+ }
117
+ /** Generic audited mutation contract supplied by the host composition root. */
118
+ export type FormCommandExecutorPort = typeof import("../core/index.js").executeCommand;
119
+ /** Authoring stays behind a port so the universal write service never loads an HTML parser. */
120
+ export interface FormAuthoringPort {
121
+ prepare(required: {
122
+ deps: Pick<import("./write-service.js").FormWriteServiceDeps, "authorize">;
123
+ input: import("./fields-json.js").FormAuthoring & {
124
+ workspaceId: string;
125
+ actor: {
126
+ id: string;
127
+ };
128
+ };
129
+ existing?: import("./fields-json.js").HtmlFormDefinitionRecord;
130
+ }, optional?: Record<string, never>): Promise<(import("./fields-json.js").FormAuthoring & {
131
+ fields?: FormDefinitionRecord["fields"];
132
+ }) | undefined>;
133
+ with(required: {
134
+ repo: FormDefinitionRepoPort;
135
+ authoring: import("./fields-json.js").FormAuthoring & {
136
+ fields?: FormDefinitionRecord["fields"];
137
+ };
138
+ }, optional?: {
139
+ replaceFields?: boolean;
140
+ }): FormDefinitionRepoPort;
141
+ }
142
+ /** A submission-only view; never changes the durable definition's descriptors. */
143
+ export type FormDefinitionViewPort = (required: {
144
+ definition: FormDefinitionRecord;
145
+ body: Record<string, unknown>;
146
+ }, optional?: Record<string, never>) => FormDefinitionRecord;
147
+ /** Host-bound Trash removal; only the host's purge lifecycle permanently deletes rows. */
148
+ export type RemoveFormSubmissionFn = (required: {
149
+ workspaceId: UUID;
150
+ id: UUID;
151
+ display: {
152
+ title: string;
153
+ subtitle?: string | null;
154
+ };
155
+ at: string;
156
+ expectedVersion: number | null;
157
+ actor: {
158
+ principalId: string;
159
+ pluginId?: string | null;
160
+ };
161
+ }) => Promise<{
162
+ ok: true;
163
+ version: number | null;
164
+ } | {
165
+ ok: false;
166
+ reason: "not-found" | "version-changed";
167
+ }>;
168
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1,8 @@
1
+ /** Builds a per-source, per-form key; rate-limit windows and quotas belong to the host. */
2
+ /** Include the form id so a visitor submitting to two forms from the same IP is never
3
+ * cross-throttled by the other form's submissions. */
4
+ export declare function buildFormsRateLimitKey({ sourceIp, formDefinitionId }: {
5
+ sourceIp: string;
6
+ formDefinitionId: string;
7
+ }): string;
8
+ //# sourceMappingURL=rate-limit-key.d.ts.map
@@ -0,0 +1,7 @@
1
+ /** Builds a per-source, per-form key; rate-limit windows and quotas belong to the host. */
2
+ /** Include the form id so a visitor submitting to two forms from the same IP is never
3
+ * cross-throttled by the other form's submissions. */
4
+ export function buildFormsRateLimitKey({ sourceIp, formDefinitionId }) {
5
+ return `${sourceIp}:${formDefinitionId}`;
6
+ }
7
+ //# sourceMappingURL=rate-limit-key.js.map
@@ -0,0 +1,120 @@
1
+ import type { UUID } from "@jini-ai/core/primitives";
2
+ import type { FormDefinitionRepoPort, FormSubmissionRepoPort } from "./ports.js";
3
+ import type { FormDefinitionRecord, FormSubmissionPage, FormSubmissionRecord } from "./types.js";
4
+ /**
5
+ * @file In-memory adapters for `forms` (rule-of-two half #1, ADR-006).
6
+ *
7
+ * Purpose:
8
+ * Test/dev-default implementations of `FormDefinitionRepoPort`/`FormSubmissionRepoPort`. Emulates
9
+ * the real DB unique index on `(workspaceId, slug)` (behavior.spec.md §6.1) so `create` throws the
10
+ * same `FormSlugConflictError` shape a `repo.sqlite.ts` unique-constraint violation would map to —
11
+ * write-service.ts's error handling is identical against either adapter.
12
+ *
13
+ * `InMemoryFormDefinitionRepo` stores `deletedAt` internally (mirroring `form_definitions.deleted_at`,
14
+ * owner ruling 2026-09-21) but never returns it through `FormDefinitionRepoPort` — those methods are
15
+ * trash-aware and fail-closed, same as `repo.sqlite.ts`'s `isNull(deletedAt)` reads. `findAnyById`/
16
+ * `save`/`hardDelete` are the deliberate, memory-ONLY exception: a trash-blind seam for the hermetic
17
+ * composition's `createRecordStoreTrashAdapter` (`@jini-ai/cms/trash`), which
18
+ * needs to see and flip a hidden row directly. They are not part of `FormDefinitionRepoPort`.
19
+ */
20
+ /** Internal storage shape — the one place in this file allowed to know about `deletedAt`. */
21
+ interface StoredFormDefinition extends FormDefinitionRecord {
22
+ deletedAt: string | null;
23
+ }
24
+ export declare class InMemoryFormDefinitionRepo implements FormDefinitionRepoPort {
25
+ private readonly rows;
26
+ /** Trash-aware: never returns a row with `deletedAt` set. */
27
+ findById(required: {
28
+ workspaceId: UUID;
29
+ id: UUID;
30
+ }): Promise<FormDefinitionRecord | null>;
31
+ /** Trash-aware: never returns a row with `deletedAt` set. */
32
+ findBySlug(required: {
33
+ workspaceId: UUID;
34
+ slug: string;
35
+ }): Promise<FormDefinitionRecord | null>;
36
+ /** Trash-aware: never lists a row with `deletedAt` set. */
37
+ list(required: {
38
+ workspaceId: UUID;
39
+ }): Promise<FormDefinitionRecord[]>;
40
+ create(record: FormDefinitionRecord): Promise<void>;
41
+ /**
42
+ * Update-only path for a LIVE row. A stale in-hand copy of a row that has since been trashed is
43
+ * silently a no-op (mirrors `repo.sqlite.ts`'s `isNull(deletedAt)` WHERE guard) — it can never
44
+ * clear a marker it never knew was set. A record with no existing row at all is still accepted
45
+ * (pre-existing behavior of this in-memory double, unrelated to trashing).
46
+ */
47
+ update(record: FormDefinitionRecord): Promise<void>;
48
+ /** Trash-BLIND — see `ports.ts`'s doc for why this is the one deliberate exception. */
49
+ isSlugTaken(required: {
50
+ workspaceId: UUID;
51
+ slug: string;
52
+ }): Promise<boolean>;
53
+ /**
54
+ * Trash-BLIND — memory-only, NOT part of `FormDefinitionRepoPort`. The hermetic composition's
55
+ * `createRecordStoreTrashAdapter` reads through this to flip `deletedAt` directly, the same way
56
+ * the real adapter reads columns without going through the trash-aware port reads.
57
+ */
58
+ findAnyById(required: {
59
+ workspaceId: UUID;
60
+ id: UUID;
61
+ }): Promise<StoredFormDefinition | null>;
62
+ /** Trash-BLIND write — memory-only, NOT part of `FormDefinitionRepoPort`. See `findAnyById`'s doc. */
63
+ save(record: StoredFormDefinition): Promise<void>;
64
+ /** Memory-only, NOT part of `FormDefinitionRepoPort`. Removes only the definition row — the
65
+ * composition root pairs this with `InMemoryFormSubmissionRepo.deleteAllForDefinition` the same
66
+ * way `features/trash/adapters/form.ts` deletes submissions before the definition. */
67
+ hardDelete(required: {
68
+ workspaceId: UUID;
69
+ id: UUID;
70
+ }): Promise<void>;
71
+ }
72
+ /** A stored submission: the port's record plus the two Trash columns only the Trash touches. */
73
+ export interface StoredFormSubmission extends FormSubmissionRecord {
74
+ deletedAt: string | null;
75
+ version: number;
76
+ }
77
+ export declare class InMemoryFormSubmissionRepo implements FormSubmissionRepoPort {
78
+ private readonly rows;
79
+ private turn;
80
+ /** Trash-aware, like `repo.sqlite.ts`: a trashed submission reads as missing. */
81
+ findById(required: {
82
+ workspaceId: UUID;
83
+ id: UUID;
84
+ }): Promise<FormSubmissionRecord | null>;
85
+ create(record: FormSubmissionRecord): Promise<void>;
86
+ /** Rolls this repo's rows back when `work` throws, as the SQL adapter's transaction does. Calls
87
+ * take turns (as the SQLite kernel's do) so one rollback cannot erase another call's row; a
88
+ * nested call would wait on itself. Other in-memory stores (the outbox) are not covered.
89
+ * @complexity O(n) snapshot of the stored rows. */
90
+ transaction<T>(work: () => Promise<T>): Promise<T>;
91
+ listByDefinition(required: {
92
+ workspaceId: UUID;
93
+ formDefinitionId: UUID;
94
+ limit: number;
95
+ }, optional?: {
96
+ cursor?: string | null;
97
+ }): Promise<FormSubmissionPage>;
98
+ /** Trash-BLIND — memory-only, NOT part of `FormSubmissionRepoPort`. The hermetic composition's
99
+ * record-store `TrashAdapter` reads and writes the marker through these three, the way the real
100
+ * adapter reads columns without going through the trash-aware port reads. */
101
+ findAnyById(required: {
102
+ workspaceId: UUID;
103
+ id: UUID;
104
+ }): Promise<StoredFormSubmission | null>;
105
+ /** Trash-BLIND write — see `findAnyById`. */
106
+ save(record: StoredFormSubmission): Promise<void>;
107
+ /** Trash-BLIND purge — see `findAnyById`. */
108
+ hardDelete(required: {
109
+ workspaceId: UUID;
110
+ id: UUID;
111
+ }): Promise<void>;
112
+ /** Memory-only, NOT part of `FormSubmissionRepoPort`. Bulk cascade for a form purge — see
113
+ * `InMemoryFormDefinitionRepo.hardDelete`'s doc for why the composition root pairs the two. */
114
+ deleteAllForDefinition(required: {
115
+ workspaceId: UUID;
116
+ formDefinitionId: UUID;
117
+ }): Promise<void>;
118
+ }
119
+ export {};
120
+ //# sourceMappingURL=repo.memory.d.ts.map
@@ -0,0 +1,188 @@
1
+ // The local trash fork was removed; @jini-ai/cms/trash owns the shared adapter/hooks.
2
+ import { encodeKeysetCursor } from "../core/keyset-cursor.js";
3
+ import { FormSlugConflictError } from "./errors.js";
4
+ function cloneStored(row) {
5
+ return {
6
+ ...row,
7
+ fields: row.fields.map((f) => ({ ...f })),
8
+ notify: { ...row.notify, recipients: [...row.notify.recipients] },
9
+ };
10
+ }
11
+ /** Strips the internal-only `deletedAt` before a row crosses `FormDefinitionRepoPort`'s boundary. */
12
+ function toPublicRecord(row) {
13
+ const { deletedAt: _deletedAt, ...record } = cloneStored(row);
14
+ return record;
15
+ }
16
+ export class InMemoryFormDefinitionRepo {
17
+ rows = new Map();
18
+ /** Trash-aware: never returns a row with `deletedAt` set. */
19
+ async findById(required) {
20
+ const row = this.rows.get(required.id);
21
+ if (!row || row.workspaceId !== required.workspaceId || row.deletedAt !== null)
22
+ return null;
23
+ return toPublicRecord(row);
24
+ }
25
+ /** Trash-aware: never returns a row with `deletedAt` set. */
26
+ async findBySlug(required) {
27
+ for (const row of this.rows.values()) {
28
+ if (row.workspaceId === required.workspaceId && row.slug === required.slug && row.deletedAt === null) {
29
+ return toPublicRecord(row);
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+ /** Trash-aware: never lists a row with `deletedAt` set. */
35
+ async list(required) {
36
+ return [...this.rows.values()]
37
+ .filter((row) => row.workspaceId === required.workspaceId && row.deletedAt === null)
38
+ .map(toPublicRecord);
39
+ }
40
+ async create(record) {
41
+ const conflicting = [...this.rows.values()].find((row) => row.workspaceId === record.workspaceId && row.slug === record.slug);
42
+ if (conflicting) {
43
+ // Trash-blind, deliberately — see `ports.ts`'s doc on `create` for why a trashed row still
44
+ // conflicts, and `repo.sqlite.ts`'s identical message for why the two adapters must agree.
45
+ const message = conflicting.deletedAt !== null
46
+ ? `a form with slug '${record.slug}' is in the Trash — restore it, or delete it permanently from the Trash, to reuse the slug`
47
+ : `a form with slug '${record.slug}' already exists`;
48
+ throw new FormSlugConflictError({ message, slug: record.slug });
49
+ }
50
+ this.rows.set(record.id, { ...cloneStored({ ...record, deletedAt: null }) });
51
+ }
52
+ /**
53
+ * Update-only path for a LIVE row. A stale in-hand copy of a row that has since been trashed is
54
+ * silently a no-op (mirrors `repo.sqlite.ts`'s `isNull(deletedAt)` WHERE guard) — it can never
55
+ * clear a marker it never knew was set. A record with no existing row at all is still accepted
56
+ * (pre-existing behavior of this in-memory double, unrelated to trashing).
57
+ */
58
+ async update(record) {
59
+ const existing = this.rows.get(record.id);
60
+ if (existing && existing.deletedAt !== null)
61
+ return;
62
+ this.rows.set(record.id, cloneStored({ ...record, deletedAt: existing?.deletedAt ?? null }));
63
+ }
64
+ /** Trash-BLIND — see `ports.ts`'s doc for why this is the one deliberate exception. */
65
+ async isSlugTaken(required) {
66
+ for (const row of this.rows.values()) {
67
+ if (row.workspaceId === required.workspaceId && row.slug === required.slug)
68
+ return true;
69
+ }
70
+ return false;
71
+ }
72
+ /**
73
+ * Trash-BLIND — memory-only, NOT part of `FormDefinitionRepoPort`. The hermetic composition's
74
+ * `createRecordStoreTrashAdapter` reads through this to flip `deletedAt` directly, the same way
75
+ * the real adapter reads columns without going through the trash-aware port reads.
76
+ */
77
+ async findAnyById(required) {
78
+ const row = this.rows.get(required.id);
79
+ if (!row || row.workspaceId !== required.workspaceId)
80
+ return null;
81
+ return cloneStored(row);
82
+ }
83
+ /** Trash-BLIND write — memory-only, NOT part of `FormDefinitionRepoPort`. See `findAnyById`'s doc. */
84
+ async save(record) {
85
+ this.rows.set(record.id, cloneStored(record));
86
+ }
87
+ /** Memory-only, NOT part of `FormDefinitionRepoPort`. Removes only the definition row — the
88
+ * composition root pairs this with `InMemoryFormSubmissionRepo.deleteAllForDefinition` the same
89
+ * way `features/trash/adapters/form.ts` deletes submissions before the definition. */
90
+ async hardDelete(required) {
91
+ const row = this.rows.get(required.id);
92
+ if (row && row.workspaceId === required.workspaceId) {
93
+ this.rows.delete(required.id);
94
+ }
95
+ }
96
+ }
97
+ function cloneSubmission(row) {
98
+ return { ...row, data: { ...row.data } };
99
+ }
100
+ /** The port's view of a stored row: the Trash columns stay inside this adapter. */
101
+ function toSubmissionRecord(row) {
102
+ const { deletedAt: _deletedAt, version: _version, ...record } = row;
103
+ return { ...record, data: { ...record.data } };
104
+ }
105
+ export class InMemoryFormSubmissionRepo {
106
+ rows = new Map();
107
+ turn = Promise.resolve();
108
+ /** Trash-aware, like `repo.sqlite.ts`: a trashed submission reads as missing. */
109
+ async findById(required) {
110
+ const row = this.rows.get(required.id);
111
+ if (!row || row.workspaceId !== required.workspaceId || row.deletedAt !== null)
112
+ return null;
113
+ return toSubmissionRecord(row);
114
+ }
115
+ async create(record) {
116
+ this.rows.set(record.id, { ...record, data: { ...record.data }, deletedAt: null, version: 1 });
117
+ }
118
+ /** Rolls this repo's rows back when `work` throws, as the SQL adapter's transaction does. Calls
119
+ * take turns (as the SQLite kernel's do) so one rollback cannot erase another call's row; a
120
+ * nested call would wait on itself. Other in-memory stores (the outbox) are not covered.
121
+ * @complexity O(n) snapshot of the stored rows. */
122
+ transaction(work) {
123
+ const run = this.turn.then(async () => {
124
+ const snapshot = new Map(this.rows);
125
+ try {
126
+ return await work();
127
+ }
128
+ catch (error) {
129
+ this.rows.clear();
130
+ for (const [id, row] of snapshot)
131
+ this.rows.set(id, row);
132
+ throw error;
133
+ }
134
+ });
135
+ this.turn = run.catch(() => undefined);
136
+ return run;
137
+ }
138
+ async listByDefinition(required, optional = {}) {
139
+ const all = [...this.rows.values()]
140
+ .filter((row) => row.workspaceId === required.workspaceId &&
141
+ row.formDefinitionId === required.formDefinitionId &&
142
+ row.deletedAt === null)
143
+ // Newest-first (behavior.spec.md §2.1): submittedAt desc, id desc tie-break.
144
+ .sort((a, b) => {
145
+ if (a.submittedAt !== b.submittedAt)
146
+ return b.submittedAt.localeCompare(a.submittedAt);
147
+ return b.id.localeCompare(a.id);
148
+ });
149
+ // Keep the published id cursor (unknown ids restart at the first page). Internally the
150
+ // shared keyset owner identifies a position by its timestamp and id, without another codec.
151
+ const cursorRow = optional.cursor ? all.find((row) => row.id === optional.cursor) : undefined;
152
+ const position = cursorRow ? encodeKeysetCursor({ createdAt: cursorRow.submittedAt, id: cursorRow.id }) : undefined;
153
+ const startIndex = position
154
+ ? all.findIndex((row) => encodeKeysetCursor({ createdAt: row.submittedAt, id: row.id }) === position) + 1
155
+ : 0;
156
+ const page = all.slice(startIndex, startIndex + required.limit);
157
+ const nextCursor = startIndex + required.limit < all.length ? page[page.length - 1]?.id ?? null : null;
158
+ return { items: page.map(toSubmissionRecord), nextCursor };
159
+ }
160
+ /** Trash-BLIND — memory-only, NOT part of `FormSubmissionRepoPort`. The hermetic composition's
161
+ * record-store `TrashAdapter` reads and writes the marker through these three, the way the real
162
+ * adapter reads columns without going through the trash-aware port reads. */
163
+ async findAnyById(required) {
164
+ const row = this.rows.get(required.id);
165
+ return row && row.workspaceId === required.workspaceId ? cloneSubmission(row) : null;
166
+ }
167
+ /** Trash-BLIND write — see `findAnyById`. */
168
+ async save(record) {
169
+ this.rows.set(record.id, cloneSubmission(record));
170
+ }
171
+ /** Trash-BLIND purge — see `findAnyById`. */
172
+ async hardDelete(required) {
173
+ const row = this.rows.get(required.id);
174
+ if (row && row.workspaceId === required.workspaceId) {
175
+ this.rows.delete(required.id);
176
+ }
177
+ }
178
+ /** Memory-only, NOT part of `FormSubmissionRepoPort`. Bulk cascade for a form purge — see
179
+ * `InMemoryFormDefinitionRepo.hardDelete`'s doc for why the composition root pairs the two. */
180
+ async deleteAllForDefinition(required) {
181
+ for (const [id, row] of this.rows) {
182
+ if (row.workspaceId === required.workspaceId && row.formDefinitionId === required.formDefinitionId) {
183
+ this.rows.delete(id);
184
+ }
185
+ }
186
+ }
187
+ }
188
+ //# sourceMappingURL=repo.memory.js.map
@@ -0,0 +1,5 @@
1
+ export * from "./tables.js";
2
+ export * from "./repo.js";
3
+ export * from "./repo.rows.js";
4
+ export * from "./submission-ip-retention-repo.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ export * from "./tables.js";
2
+ export * from "./repo.js";
3
+ export * from "./repo.rows.js";
4
+ export * from "./submission-ip-retention-repo.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,94 @@
1
+ import type { UUID } from "@jini-ai/core/primitives";
2
+ import type { StorageKernel } from "@jini-ai/db/kernel";
3
+ import type { FormsTables, FormDefinitionDatabase, FormSubmissionDatabase } from "./tables.js";
4
+ import type { FormDefinitionRepoPort, FormSubmissionRepoPort } from "../ports.js";
5
+ import type { FormDefinitionRecord, FormSubmissionPage, FormSubmissionRecord } from "../types.js";
6
+ /**
7
+ * @file THE forms repositories: one Kysely query body for every database the storage kernel drives
8
+ * (SQLite, PGlite, Postgres). Every statement goes through `kernel.run` and is awaited.
9
+ *
10
+ * `create`'s slug uniqueness is enforced by the real unique index
11
+ * (`form_definitions_workspace_slug_unique`) — the tie-break of behavior.spec.md §6.1 — but the
12
+ * conflict is detected by a check inside a transaction holding the workspace's forms lock, not by
13
+ * catching the violation: on Postgres a failed statement aborts the surrounding transaction, so a
14
+ * follow-up "is it in the Trash?" query could not run. The index stays as the backstop against a
15
+ * writer outside this process.
16
+ */
17
+ export declare class SqlFormDefinitionRepo implements FormDefinitionRepoPort {
18
+ protected readonly kernel: StorageKernel<FormDefinitionDatabase>;
19
+ protected readonly tables: FormsTables;
20
+ constructor({ kernel, tables }: {
21
+ kernel: StorageKernel<FormDefinitionDatabase>;
22
+ tables: FormsTables;
23
+ }, _optional?: Record<string, never>);
24
+ /** Trash-aware: `deleted_at IS NULL` excludes a trashed row (`ports.ts`'s fail-closed-reads doc). */
25
+ findById(required: {
26
+ workspaceId: UUID;
27
+ id: UUID;
28
+ }): Promise<FormDefinitionRecord | null>;
29
+ /** Trash-aware, as {@link findById}. */
30
+ findBySlug(required: {
31
+ workspaceId: UUID;
32
+ slug: string;
33
+ }): Promise<FormDefinitionRecord | null>;
34
+ /** Trash-aware, as {@link findById}. */
35
+ list(required: {
36
+ workspaceId: UUID;
37
+ }): Promise<FormDefinitionRecord[]>;
38
+ create(record: FormDefinitionRecord): Promise<void>;
39
+ /**
40
+ * Update-only path for a LIVE row. `version`/`deleted_at` are deliberately excluded from the SET
41
+ * list (only the Trash's compare-and-set may move `version`), and `deleted_at IS NULL` in the
42
+ * WHERE means the UPDATE simply matches zero rows if the record has since been trashed — a stale
43
+ * in-hand copy can never clear a marker it never knew was set.
44
+ */
45
+ update(record: FormDefinitionRecord): Promise<void>;
46
+ /** Trash-BLIND — see `ports.ts`'s doc for why this is the one deliberate exception. */
47
+ isSlugTaken(required: {
48
+ workspaceId: UUID;
49
+ slug: string;
50
+ }): Promise<boolean>;
51
+ }
52
+ export declare class SqlFormSubmissionRepo implements FormSubmissionRepoPort {
53
+ protected readonly kernel: StorageKernel<FormSubmissionDatabase>;
54
+ protected readonly tables: FormsTables;
55
+ constructor({ kernel, tables }: {
56
+ kernel: StorageKernel<FormSubmissionDatabase>;
57
+ tables: FormsTables;
58
+ }, _optional?: Record<string, never>);
59
+ findById(required: {
60
+ workspaceId: UUID;
61
+ id: UUID;
62
+ }): Promise<FormSubmissionRecord | null>;
63
+ create(record: FormSubmissionRecord): Promise<void>;
64
+ /** The content kernel's transaction; a nested call joins the open one.
65
+ * @complexity O(1) beyond `work`. */
66
+ transaction<T>(work: () => Promise<T>): Promise<T>;
67
+ /**
68
+ * Newest-first (`submitted_at` desc, `id` desc tie-break, behavior.spec.md §2.1). The cursor
69
+ * resumes strictly after the previously returned page's last row on that same ordering; an unknown
70
+ * cursor id is ignored (first page), as before.
71
+ *
72
+ * @complexity O(limit) rows read via the definition index; two statements when a cursor is given.
73
+ */
74
+ listByDefinition(required: {
75
+ workspaceId: UUID;
76
+ formDefinitionId: UUID;
77
+ limit: number;
78
+ }, optional?: {
79
+ cursor?: string | null;
80
+ }): Promise<FormSubmissionPage>;
81
+ }
82
+ /** The form-definition repo for the injected kernel and required host table names.
83
+ * @complexity O(1); no connection or schema work. */
84
+ export declare function formDefinitionRepoFor<DB>({ kernel, tables }: {
85
+ kernel: StorageKernel<DB>;
86
+ tables: FormsTables;
87
+ }, _optional?: Record<string, never>): SqlFormDefinitionRepo;
88
+ /** The form-submission repo for the injected kernel and required host table names.
89
+ * @complexity O(1); no connection or schema work. */
90
+ export declare function formSubmissionRepoFor<DB>({ kernel, tables }: {
91
+ kernel: StorageKernel<DB>;
92
+ tables: FormsTables;
93
+ }, _optional?: Record<string, never>): SqlFormSubmissionRepo;
94
+ //# sourceMappingURL=repo.d.ts.map