@form-engine-ts/storage 7.17.3 → 7.18.1

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/README.md CHANGED
@@ -20,3 +20,20 @@ through page fetching. MongoDB and Azure Table expose the same typed `fetchSubmi
20
20
  Both adapters implement the required `UnifiedSubmissionStorageAdapter` surface: typed submission paging and filters,
21
21
  free-text answer paging, idempotent saving, typed validation, response aggregation, and CSV export. These common
22
22
  operations use the same core contracts regardless of the backing database.
23
+
24
+ The `./testing` subpath exports framework-independent JSON fixtures and contract runners for pagination,
25
+ idempotency, revision conflicts, translation metadata, CSV, and form deletion. Adapters expose
26
+ `inspectFormDeletion` and `deleteForm` through the lifecycle contract when their implementation supports it;
27
+ transactional deletion is required by default and `allowNonAtomic: true` is an explicit fallback.
28
+
29
+ | Package | 7.18.x contract status |
30
+ | --- | --- |
31
+ | `storage-memory` | lifecycle, pagination, idempotency |
32
+ | `storage-mongodb` | lifecycle, version state/events, transactions |
33
+ | `storage-azure-table` | lifecycle, pagination; no native transaction |
34
+ | `storage-postgres` / `storage-sqlite` / `storage-d1` | lifecycle, pagination; transaction depends on injected client |
35
+ | `storage-localstorage` | lifecycle, pagination; non-atomic only |
36
+
37
+ Existing `StorageAdapter` methods remain valid. To migrate, pass `schemaValidation` to an adapter when
38
+ Content Mode policy must be enforced at persistence, then use `inspectFormDeletion` before deletion and
39
+ handle `transaction_unsupported` explicitly for adapters without transactions.
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/testing.ts
21
+ var testing_exports = {};
22
+ __export(testing_exports, {
23
+ createStorageContractFixtures: () => createStorageContractFixtures,
24
+ runIdempotencyContract: () => runIdempotencyContract,
25
+ runLifecycleContract: () => runLifecycleContract,
26
+ runRevisionConflictContract: () => runRevisionConflictContract,
27
+ runStorageContract: () => runStorageContract,
28
+ storageContractScope: () => storageContractScope,
29
+ verifyStorageCodecVectors: () => verifyStorageCodecVectors
30
+ });
31
+ module.exports = __toCommonJS(testing_exports);
32
+ var import_core = require("@form-engine-ts/core");
33
+ function createStorageContractFixtures() {
34
+ const schema = {
35
+ ...(0, import_core.createInitialSchemaByMode)("poll", { id: "contract-form", title: "Contract", locale: "en" }),
36
+ supportedLocales: ["en", "ja"],
37
+ metadata: { mode: "poll", tenantId: "tenant-a", ownerId: "owner-a", custom: { nested: [true, 2, "three"] } },
38
+ translationMetadata: { ja: { title: { provider: "fixture", translationSource: "manual", custom: "retained" } } }
39
+ };
40
+ const submissions = ["a", "b", "c"].map((id) => ({
41
+ id: `contract-${id}`,
42
+ formId: schema.id,
43
+ formVersion: schema.version,
44
+ locale: id === "c" ? "ja" : "en",
45
+ submittedAt: "2026-01-01T00:00:00.000Z",
46
+ values: { "question-1": "option-1" },
47
+ metadata: { tenantId: "tenant-a", ownerId: "owner-a", custom: { source: id } },
48
+ translationMetadata: { ja: { "question-1": { provider: "fixture", translationSource: "manual" } } }
49
+ }));
50
+ const state = { formId: schema.id, nextVersion: 2, revision: 0, publishedVersion: 1 };
51
+ return {
52
+ schema,
53
+ submissions,
54
+ state,
55
+ revisionConflict: { expectedRevision: 1, actualRevision: 0 },
56
+ cursor: { submittedAt: "2026-01-01T00:00:00.000Z", responseId: "contract-a" },
57
+ invalidCursors: ["not-a-cursor", "%", "{}"],
58
+ csv: {
59
+ options: { withBom: false },
60
+ expected: "submissionId,submittedAt,locale,question-1\r\ncontract-a,2026-01-01T00:00:00.000Z,en,option-1\r\ncontract-b,2026-01-01T00:00:00.000Z,en,option-1\r\ncontract-c,2026-01-01T00:00:00.000Z,ja,option-1"
61
+ }
62
+ };
63
+ }
64
+ function storageContractScope(resource) {
65
+ const value = resource.value;
66
+ if (typeof value !== "object" || value === null || !("metadata" in value)) return {};
67
+ const metadata = value.metadata;
68
+ if (typeof metadata !== "object" || metadata === null) return {};
69
+ return {
70
+ ..."tenantId" in metadata && typeof metadata.tenantId === "string" ? { tenantId: metadata.tenantId } : {},
71
+ ..."ownerId" in metadata && typeof metadata.ownerId === "string" ? { ownerId: metadata.ownerId } : {}
72
+ };
73
+ }
74
+ function check(condition, message) {
75
+ if (!condition) throw new Error(`Storage contract: ${message}`);
76
+ }
77
+ async function runStorageContract(adapter) {
78
+ const { schema, submissions } = createStorageContractFixtures();
79
+ await adapter.saveSchema(schema);
80
+ const stored = await adapter.getSchema(schema.id, schema.version);
81
+ check(JSON.stringify(stored) === JSON.stringify(schema), "schema JSON/translation metadata round trip");
82
+ for (const submission of submissions) await adapter.saveSubmission(submission);
83
+ const loaded = await adapter.listSubmissions(schema.id, schema.version);
84
+ check(JSON.stringify(loaded) === JSON.stringify(submissions), "submission ordering/metadata round trip");
85
+ const passed = ["schema", "submission", "translation_metadata"];
86
+ const unsupported = [];
87
+ if ("listSubmissionPage" in adapter && typeof adapter.listSubmissionPage === "function") {
88
+ const paged = adapter;
89
+ const ids = [];
90
+ let cursor;
91
+ for (let index = 0; index < 10; index++) {
92
+ const page = await paged.listSubmissionPage(schema.id, {
93
+ pageSize: 1,
94
+ ...cursor === void 0 ? {} : { cursor }
95
+ });
96
+ ids.push(...page.items.map((item) => item.id));
97
+ if (!page.hasMore) break;
98
+ check(page.nextCursor !== void 0 && page.nextCursor !== cursor, "pagination must make progress");
99
+ cursor = page.nextCursor;
100
+ }
101
+ check(
102
+ JSON.stringify(ids) === JSON.stringify(submissions.map((item) => item.id)),
103
+ "pagination must not lose or repeat records"
104
+ );
105
+ passed.push("pagination");
106
+ } else unsupported.push("pagination");
107
+ return { passed, unsupported };
108
+ }
109
+ async function runIdempotencyContract(save) {
110
+ const [submission] = createStorageContractFixtures().submissions;
111
+ if (submission === void 0) throw new Error("Missing fixture");
112
+ check((await save(submission))?.status === "created", "first idempotent save");
113
+ check((await save(submission))?.status === "duplicate", "retry must be a duplicate");
114
+ check(
115
+ (await save({ ...submission, values: { "question-1": "option-2" } }))?.status === "conflict",
116
+ "changed payload must conflict"
117
+ );
118
+ }
119
+ async function runRevisionConflictContract(adapter) {
120
+ const { schema, state } = createStorageContractFixtures();
121
+ const planned = (0, import_core.createCloneTransitionPlan)(
122
+ state,
123
+ { formId: schema.id, version: 1, status: "published", schema, revision: 1, createdAt: "2026-01-01T00:00:00.000Z" },
124
+ { expectedRevision: 0, clonedAt: "2026-01-01T00:00:00.000Z" }
125
+ );
126
+ check(planned.success, "valid transition fixture");
127
+ const first = await adapter.commitVersionTransition(planned.value.plan);
128
+ check(first.success, "first transition commits");
129
+ const retry = await adapter.commitVersionTransition(planned.value.plan);
130
+ check(!retry.success && retry.error.type === "revision_conflict", "stale transition conflicts");
131
+ }
132
+ async function runLifecycleContract(adapter) {
133
+ if (adapter.inspectFormDeletion === void 0 || adapter.deleteForm === void 0 || adapter.lifecycleCapabilities === void 0)
134
+ throw new Error("Adapter does not expose the lifecycle contract.");
135
+ const lifecycle = adapter;
136
+ const { schema, submissions } = createStorageContractFixtures();
137
+ const submission = submissions[0];
138
+ if (submission === void 0) throw new Error("Missing fixture");
139
+ await adapter.saveSchema(schema);
140
+ await adapter.saveSubmission(submission);
141
+ await adapter.saveSchema({ ...schema, id: "contract-other" });
142
+ await adapter.saveSchema({ ...schema, version: 2, metadata: { tenantId: "tenant-b", ownerId: "owner-a" } });
143
+ await adapter.saveSchema({ ...schema, version: 3, metadata: { tenantId: "tenant-a", ownerId: "owner-b" } });
144
+ const request = { formId: schema.id, tenantId: "tenant-a", ownerId: "owner-a" };
145
+ const inspection = await lifecycle.inspectFormDeletion({ ...request, pageSize: 1 });
146
+ check(inspection.counts.schema === 1 && inspection.counts.submission === 1, "scope inspection counts");
147
+ check(inspection.targets.length === 1 && inspection.nextCursor !== void 0, "inspection pagination");
148
+ const next = await lifecycle.inspectFormDeletion({ ...request, pageSize: 1, cursor: inspection.nextCursor });
149
+ check(next.targets.length === 1 && next.targets[0]?.id !== inspection.targets[0]?.id, "inspection next page");
150
+ check((await lifecycle.deleteForm({ ...request, dryRun: true })).status === "dry_run", "dry run result");
151
+ check((await adapter.listSubmissions(schema.id)).length === 1, "dry run must not mutate");
152
+ if (!lifecycle.lifecycleCapabilities.atomic) {
153
+ check(
154
+ (await lifecycle.deleteForm(request)).error?.code === "transaction_unsupported",
155
+ "atomic default rejects unsupported storage"
156
+ );
157
+ check((await adapter.listSubmissions(schema.id)).length === 1, "unsupported transaction must not mutate");
158
+ }
159
+ const result = await lifecycle.deleteForm({ ...request, allowNonAtomic: true });
160
+ check(
161
+ result.status === "deleted" && result.counts.schema === 1 && result.counts.submission === 1,
162
+ "actual deletion counts"
163
+ );
164
+ check(await adapter.getSchema("contract-other", 1) !== null, "other form survives");
165
+ check(await adapter.getSchema(schema.id, 2) !== null, "other tenant survives");
166
+ check(await adapter.getSchema(schema.id, 3) !== null, "other owner survives");
167
+ const retry = await lifecycle.deleteForm({ ...request, allowNonAtomic: true });
168
+ check(
169
+ retry.status === "deleted" && Object.values(retry.counts).every((count) => count === 0),
170
+ "deletion retry is empty"
171
+ );
172
+ }
173
+ function verifyStorageCodecVectors() {
174
+ const fixtures = createStorageContractFixtures();
175
+ check(
176
+ JSON.stringify((0, import_core.decodeSubmissionCursor)((0, import_core.encodeSubmissionCursor)(fixtures.cursor))) === JSON.stringify(fixtures.cursor),
177
+ "cursor round trip"
178
+ );
179
+ for (const cursor of fixtures.invalidCursors) {
180
+ let rejected = false;
181
+ try {
182
+ (0, import_core.decodeSubmissionCursor)(cursor);
183
+ } catch {
184
+ rejected = true;
185
+ }
186
+ check(rejected, "invalid cursor rejection");
187
+ }
188
+ check(
189
+ (0, import_core.exportResponsesToCsv)(fixtures.schema, fixtures.submissions, fixtures.csv.options) === fixtures.csv.expected,
190
+ "CSV fixture output"
191
+ );
192
+ }
193
+ // Annotate the CommonJS export names for ESM import in node:
194
+ 0 && (module.exports = {
195
+ createStorageContractFixtures,
196
+ runIdempotencyContract,
197
+ runLifecycleContract,
198
+ runRevisionConflictContract,
199
+ runStorageContract,
200
+ storageContractScope,
201
+ verifyStorageCodecVectors
202
+ });
@@ -0,0 +1,39 @@
1
+ import { FormSchema, FormSubmission, FormVersionState, SubmissionSaveResult, FormStorageAdapter, VersionedFormStorageAdapter, FormResource } from '@form-engine-ts/core';
2
+
3
+ /** Fresh JSON fixtures: callers may customize a copy without contaminating another adapter's run. */
4
+ declare function createStorageContractFixtures(): {
5
+ schema: FormSchema;
6
+ submissions: readonly FormSubmission[];
7
+ state: FormVersionState;
8
+ revisionConflict: {
9
+ expectedRevision: number;
10
+ actualRevision: number;
11
+ };
12
+ cursor: {
13
+ submittedAt: string;
14
+ responseId: string;
15
+ };
16
+ invalidCursors: string[];
17
+ csv: {
18
+ options: {
19
+ withBom: boolean;
20
+ };
21
+ expected: string;
22
+ };
23
+ };
24
+ declare function storageContractScope(resource: FormResource): {
25
+ tenantId?: string;
26
+ ownerId?: string;
27
+ };
28
+ interface StorageContractReport {
29
+ readonly passed: readonly string[];
30
+ readonly unsupported: readonly string[];
31
+ }
32
+ /** Run against an empty, disposable adapter. Never clears a caller's database. */
33
+ declare function runStorageContract(adapter: FormStorageAdapter): Promise<StorageContractReport>;
34
+ declare function runIdempotencyContract(save: (submission: FormSubmission) => Promise<undefined | SubmissionSaveResult>): Promise<void>;
35
+ declare function runRevisionConflictContract(adapter: VersionedFormStorageAdapter): Promise<void>;
36
+ declare function runLifecycleContract(adapter: FormStorageAdapter): Promise<void>;
37
+ declare function verifyStorageCodecVectors(): void;
38
+
39
+ export { type StorageContractReport, createStorageContractFixtures, runIdempotencyContract, runLifecycleContract, runRevisionConflictContract, runStorageContract, storageContractScope, verifyStorageCodecVectors };
@@ -0,0 +1,39 @@
1
+ import { FormSchema, FormSubmission, FormVersionState, SubmissionSaveResult, FormStorageAdapter, VersionedFormStorageAdapter, FormResource } from '@form-engine-ts/core';
2
+
3
+ /** Fresh JSON fixtures: callers may customize a copy without contaminating another adapter's run. */
4
+ declare function createStorageContractFixtures(): {
5
+ schema: FormSchema;
6
+ submissions: readonly FormSubmission[];
7
+ state: FormVersionState;
8
+ revisionConflict: {
9
+ expectedRevision: number;
10
+ actualRevision: number;
11
+ };
12
+ cursor: {
13
+ submittedAt: string;
14
+ responseId: string;
15
+ };
16
+ invalidCursors: string[];
17
+ csv: {
18
+ options: {
19
+ withBom: boolean;
20
+ };
21
+ expected: string;
22
+ };
23
+ };
24
+ declare function storageContractScope(resource: FormResource): {
25
+ tenantId?: string;
26
+ ownerId?: string;
27
+ };
28
+ interface StorageContractReport {
29
+ readonly passed: readonly string[];
30
+ readonly unsupported: readonly string[];
31
+ }
32
+ /** Run against an empty, disposable adapter. Never clears a caller's database. */
33
+ declare function runStorageContract(adapter: FormStorageAdapter): Promise<StorageContractReport>;
34
+ declare function runIdempotencyContract(save: (submission: FormSubmission) => Promise<undefined | SubmissionSaveResult>): Promise<void>;
35
+ declare function runRevisionConflictContract(adapter: VersionedFormStorageAdapter): Promise<void>;
36
+ declare function runLifecycleContract(adapter: FormStorageAdapter): Promise<void>;
37
+ declare function verifyStorageCodecVectors(): void;
38
+
39
+ export { type StorageContractReport, createStorageContractFixtures, runIdempotencyContract, runLifecycleContract, runRevisionConflictContract, runStorageContract, storageContractScope, verifyStorageCodecVectors };
@@ -0,0 +1,177 @@
1
+ // src/testing.ts
2
+ import {
3
+ createCloneTransitionPlan,
4
+ createInitialSchemaByMode,
5
+ decodeSubmissionCursor,
6
+ encodeSubmissionCursor,
7
+ exportResponsesToCsv
8
+ } from "@form-engine-ts/core";
9
+ function createStorageContractFixtures() {
10
+ const schema = {
11
+ ...createInitialSchemaByMode("poll", { id: "contract-form", title: "Contract", locale: "en" }),
12
+ supportedLocales: ["en", "ja"],
13
+ metadata: { mode: "poll", tenantId: "tenant-a", ownerId: "owner-a", custom: { nested: [true, 2, "three"] } },
14
+ translationMetadata: { ja: { title: { provider: "fixture", translationSource: "manual", custom: "retained" } } }
15
+ };
16
+ const submissions = ["a", "b", "c"].map((id) => ({
17
+ id: `contract-${id}`,
18
+ formId: schema.id,
19
+ formVersion: schema.version,
20
+ locale: id === "c" ? "ja" : "en",
21
+ submittedAt: "2026-01-01T00:00:00.000Z",
22
+ values: { "question-1": "option-1" },
23
+ metadata: { tenantId: "tenant-a", ownerId: "owner-a", custom: { source: id } },
24
+ translationMetadata: { ja: { "question-1": { provider: "fixture", translationSource: "manual" } } }
25
+ }));
26
+ const state = { formId: schema.id, nextVersion: 2, revision: 0, publishedVersion: 1 };
27
+ return {
28
+ schema,
29
+ submissions,
30
+ state,
31
+ revisionConflict: { expectedRevision: 1, actualRevision: 0 },
32
+ cursor: { submittedAt: "2026-01-01T00:00:00.000Z", responseId: "contract-a" },
33
+ invalidCursors: ["not-a-cursor", "%", "{}"],
34
+ csv: {
35
+ options: { withBom: false },
36
+ expected: "submissionId,submittedAt,locale,question-1\r\ncontract-a,2026-01-01T00:00:00.000Z,en,option-1\r\ncontract-b,2026-01-01T00:00:00.000Z,en,option-1\r\ncontract-c,2026-01-01T00:00:00.000Z,ja,option-1"
37
+ }
38
+ };
39
+ }
40
+ function storageContractScope(resource) {
41
+ const value = resource.value;
42
+ if (typeof value !== "object" || value === null || !("metadata" in value)) return {};
43
+ const metadata = value.metadata;
44
+ if (typeof metadata !== "object" || metadata === null) return {};
45
+ return {
46
+ ..."tenantId" in metadata && typeof metadata.tenantId === "string" ? { tenantId: metadata.tenantId } : {},
47
+ ..."ownerId" in metadata && typeof metadata.ownerId === "string" ? { ownerId: metadata.ownerId } : {}
48
+ };
49
+ }
50
+ function check(condition, message) {
51
+ if (!condition) throw new Error(`Storage contract: ${message}`);
52
+ }
53
+ async function runStorageContract(adapter) {
54
+ const { schema, submissions } = createStorageContractFixtures();
55
+ await adapter.saveSchema(schema);
56
+ const stored = await adapter.getSchema(schema.id, schema.version);
57
+ check(JSON.stringify(stored) === JSON.stringify(schema), "schema JSON/translation metadata round trip");
58
+ for (const submission of submissions) await adapter.saveSubmission(submission);
59
+ const loaded = await adapter.listSubmissions(schema.id, schema.version);
60
+ check(JSON.stringify(loaded) === JSON.stringify(submissions), "submission ordering/metadata round trip");
61
+ const passed = ["schema", "submission", "translation_metadata"];
62
+ const unsupported = [];
63
+ if ("listSubmissionPage" in adapter && typeof adapter.listSubmissionPage === "function") {
64
+ const paged = adapter;
65
+ const ids = [];
66
+ let cursor;
67
+ for (let index = 0; index < 10; index++) {
68
+ const page = await paged.listSubmissionPage(schema.id, {
69
+ pageSize: 1,
70
+ ...cursor === void 0 ? {} : { cursor }
71
+ });
72
+ ids.push(...page.items.map((item) => item.id));
73
+ if (!page.hasMore) break;
74
+ check(page.nextCursor !== void 0 && page.nextCursor !== cursor, "pagination must make progress");
75
+ cursor = page.nextCursor;
76
+ }
77
+ check(
78
+ JSON.stringify(ids) === JSON.stringify(submissions.map((item) => item.id)),
79
+ "pagination must not lose or repeat records"
80
+ );
81
+ passed.push("pagination");
82
+ } else unsupported.push("pagination");
83
+ return { passed, unsupported };
84
+ }
85
+ async function runIdempotencyContract(save) {
86
+ const [submission] = createStorageContractFixtures().submissions;
87
+ if (submission === void 0) throw new Error("Missing fixture");
88
+ check((await save(submission))?.status === "created", "first idempotent save");
89
+ check((await save(submission))?.status === "duplicate", "retry must be a duplicate");
90
+ check(
91
+ (await save({ ...submission, values: { "question-1": "option-2" } }))?.status === "conflict",
92
+ "changed payload must conflict"
93
+ );
94
+ }
95
+ async function runRevisionConflictContract(adapter) {
96
+ const { schema, state } = createStorageContractFixtures();
97
+ const planned = createCloneTransitionPlan(
98
+ state,
99
+ { formId: schema.id, version: 1, status: "published", schema, revision: 1, createdAt: "2026-01-01T00:00:00.000Z" },
100
+ { expectedRevision: 0, clonedAt: "2026-01-01T00:00:00.000Z" }
101
+ );
102
+ check(planned.success, "valid transition fixture");
103
+ const first = await adapter.commitVersionTransition(planned.value.plan);
104
+ check(first.success, "first transition commits");
105
+ const retry = await adapter.commitVersionTransition(planned.value.plan);
106
+ check(!retry.success && retry.error.type === "revision_conflict", "stale transition conflicts");
107
+ }
108
+ async function runLifecycleContract(adapter) {
109
+ if (adapter.inspectFormDeletion === void 0 || adapter.deleteForm === void 0 || adapter.lifecycleCapabilities === void 0)
110
+ throw new Error("Adapter does not expose the lifecycle contract.");
111
+ const lifecycle = adapter;
112
+ const { schema, submissions } = createStorageContractFixtures();
113
+ const submission = submissions[0];
114
+ if (submission === void 0) throw new Error("Missing fixture");
115
+ await adapter.saveSchema(schema);
116
+ await adapter.saveSubmission(submission);
117
+ await adapter.saveSchema({ ...schema, id: "contract-other" });
118
+ await adapter.saveSchema({ ...schema, version: 2, metadata: { tenantId: "tenant-b", ownerId: "owner-a" } });
119
+ await adapter.saveSchema({ ...schema, version: 3, metadata: { tenantId: "tenant-a", ownerId: "owner-b" } });
120
+ const request = { formId: schema.id, tenantId: "tenant-a", ownerId: "owner-a" };
121
+ const inspection = await lifecycle.inspectFormDeletion({ ...request, pageSize: 1 });
122
+ check(inspection.counts.schema === 1 && inspection.counts.submission === 1, "scope inspection counts");
123
+ check(inspection.targets.length === 1 && inspection.nextCursor !== void 0, "inspection pagination");
124
+ const next = await lifecycle.inspectFormDeletion({ ...request, pageSize: 1, cursor: inspection.nextCursor });
125
+ check(next.targets.length === 1 && next.targets[0]?.id !== inspection.targets[0]?.id, "inspection next page");
126
+ check((await lifecycle.deleteForm({ ...request, dryRun: true })).status === "dry_run", "dry run result");
127
+ check((await adapter.listSubmissions(schema.id)).length === 1, "dry run must not mutate");
128
+ if (!lifecycle.lifecycleCapabilities.atomic) {
129
+ check(
130
+ (await lifecycle.deleteForm(request)).error?.code === "transaction_unsupported",
131
+ "atomic default rejects unsupported storage"
132
+ );
133
+ check((await adapter.listSubmissions(schema.id)).length === 1, "unsupported transaction must not mutate");
134
+ }
135
+ const result = await lifecycle.deleteForm({ ...request, allowNonAtomic: true });
136
+ check(
137
+ result.status === "deleted" && result.counts.schema === 1 && result.counts.submission === 1,
138
+ "actual deletion counts"
139
+ );
140
+ check(await adapter.getSchema("contract-other", 1) !== null, "other form survives");
141
+ check(await adapter.getSchema(schema.id, 2) !== null, "other tenant survives");
142
+ check(await adapter.getSchema(schema.id, 3) !== null, "other owner survives");
143
+ const retry = await lifecycle.deleteForm({ ...request, allowNonAtomic: true });
144
+ check(
145
+ retry.status === "deleted" && Object.values(retry.counts).every((count) => count === 0),
146
+ "deletion retry is empty"
147
+ );
148
+ }
149
+ function verifyStorageCodecVectors() {
150
+ const fixtures = createStorageContractFixtures();
151
+ check(
152
+ JSON.stringify(decodeSubmissionCursor(encodeSubmissionCursor(fixtures.cursor))) === JSON.stringify(fixtures.cursor),
153
+ "cursor round trip"
154
+ );
155
+ for (const cursor of fixtures.invalidCursors) {
156
+ let rejected = false;
157
+ try {
158
+ decodeSubmissionCursor(cursor);
159
+ } catch {
160
+ rejected = true;
161
+ }
162
+ check(rejected, "invalid cursor rejection");
163
+ }
164
+ check(
165
+ exportResponsesToCsv(fixtures.schema, fixtures.submissions, fixtures.csv.options) === fixtures.csv.expected,
166
+ "CSV fixture output"
167
+ );
168
+ }
169
+ export {
170
+ createStorageContractFixtures,
171
+ runIdempotencyContract,
172
+ runLifecycleContract,
173
+ runRevisionConflictContract,
174
+ runStorageContract,
175
+ storageContractScope,
176
+ verifyStorageCodecVectors
177
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/storage",
3
- "version": "7.17.3",
3
+ "version": "7.18.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -29,6 +29,11 @@
29
29
  "types": "./dist/pagination.d.ts",
30
30
  "import": "./dist/pagination.js",
31
31
  "require": "./dist/pagination.cjs"
32
+ },
33
+ "./testing": {
34
+ "types": "./dist/testing.d.ts",
35
+ "import": "./dist/testing.js",
36
+ "require": "./dist/testing.cjs"
32
37
  }
33
38
  },
34
39
  "license": "MIT",
@@ -37,10 +42,10 @@
37
42
  "url": "git+https://github.com/nitta-a/form-engine-ts.git"
38
43
  },
39
44
  "dependencies": {
40
- "@form-engine-ts/core": "7.17.3"
45
+ "@form-engine-ts/core": "7.18.1"
41
46
  },
42
47
  "scripts": {
43
- "build": "tsup src/index.ts src/types.ts src/pagination.ts --format esm,cjs --dts --clean --external @form-engine-ts/core",
48
+ "build": "tsup src/index.ts src/types.ts src/pagination.ts src/testing.ts --format esm,cjs --dts --clean --external @form-engine-ts/core",
44
49
  "check": "biome check . && tsc --noEmit",
45
50
  "test": "vitest run --globals",
46
51
  "typecheck": "tsc --noEmit"