@form-engine-ts/core 2.5.1 → 2.6.0

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
@@ -76,17 +76,19 @@ Results are ordered by `submittedAt`, then submission ID. Both boundaries are in
76
76
 
77
77
  ## Versioning, incremental analytics, and paged storage
78
78
 
79
- `cloneVersionToDraft`, `publishDraft`, and `deleteDraft` implement revision-checked version transitions as pure functions.
79
+ `cloneVersionToDraft`, asynchronous `publishDraft`, and `deleteDraft` implement revision-checked version transitions as
80
+ pure functions.
80
81
  Clone/delete operations accept `expectedRevision`; cloning rejects non-published sources, publish validation failures are
81
- returned as typed `validation_failed` issues, and successful publishing reports every archived record. Storage adapters
82
- that can commit the resulting changes atomically implement `VersionedFormStorageAdapter` and `VersionTransitionPlan`.
82
+ returned as typed `validation_failed` issues, and successful publishing archives only a supplied actual published record,
83
+ preserving its schema and metadata. `createPublishTransitionPlan` returns complete records plus expected/next revisions for
84
+ storage adapters implementing `VersionedFormStorageAdapter` to commit atomically.
83
85
  `createResponseAccumulator` incrementally counts choices, answered/unanswered values, and numeric summaries without retaining
84
86
  free-text bodies. In lenient mode, mismatched responses are skipped and exposed by `addMany()` and `getReport()` instead of
85
87
  being included silently. Independent accumulators for the same schema can be merged, and `finalize()` matches
86
88
  `aggregateResponses`.
87
89
 
88
90
  `exportResponsesToCsvStream` accepts an `AsyncIterable`, emits the BOM/header and one chunk per response, and supports
89
- custom `CsvColumnDef` columns. Custom getters receive the submission, form version, and schema. Use
91
+ custom `CsvColumnDef` columns. Custom getters may be asynchronous and receive the submission, form version, and schema. Use
90
92
  `pipeResponsesToCsvStream` to write to a Web `WritableStream` or Node-compatible writable while honoring backpressure.
91
93
  Formula-injection neutralization applies to both default and custom columns.
92
94
 
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ __export(index_exports, {
30
30
  calculatePageVisibility: () => calculatePageVisibility,
31
31
  cloneVersionToDraft: () => cloneVersionToDraft,
32
32
  collectSchemaLocales: () => collectSchemaLocales,
33
+ createPublishTransitionPlan: () => createPublishTransitionPlan,
33
34
  createResponseAccumulator: () => createResponseAccumulator,
34
35
  createSubmission: () => createSubmission,
35
36
  decodeSubmissionCursor: () => decodeSubmissionCursor,
@@ -1346,7 +1347,7 @@ async function* exportResponsesToCsvStream(schema, submissions, options = {}) {
1346
1347
  formVersion: response.formVersion ?? schema.version,
1347
1348
  schema
1348
1349
  };
1349
- const customCells = customColumns.map((column) => column.getValue(context));
1350
+ const customCells = await Promise.all(customColumns.map((column) => column.getValue(context)));
1350
1351
  yield `\r
1351
1352
  ${[...defaultCells, ...customCells].map((value) => escapeCsvCell(value, neutralizeFormulas)).join(",")}`;
1352
1353
  }
@@ -2091,31 +2092,39 @@ function cloneVersionToDraft(state, sourceSchema, options = {}) {
2091
2092
  }
2092
2093
  };
2093
2094
  }
2094
- function publishDraft(state, draftSchema, options = {}) {
2095
+ function validatePublishedRecord(state, record) {
2096
+ if (record === void 0) return;
2097
+ if (record.formId !== state.formId || record.version !== state.publishedVersion || record.status !== "published" || record.schema.id !== record.formId || record.schema.version !== record.version) {
2098
+ throw new TypeError("currentPublishedRecord must match the state's published version.");
2099
+ }
2100
+ }
2101
+ function transitionTimestamp(options) {
2102
+ const timestamp = options.publishedAt ?? options.timestamp ?? "1970-01-01T00:00:00.000Z";
2103
+ if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("publishedAt must be a valid date string.");
2104
+ return timestamp;
2105
+ }
2106
+ async function publishDraft(state, draftSchema, options = {}) {
2095
2107
  validateState(state);
2096
2108
  const conflict = revisionConflict(state, options.expectedRevision);
2097
2109
  if (conflict !== void 0) return conflict;
2098
2110
  if (state.draftVersion === void 0 || draftSchema.id !== state.formId || draftSchema.version !== state.draftVersion) {
2099
2111
  return { success: false, error: { type: "draft_not_found" } };
2100
2112
  }
2101
- const validation = options.validate?.(draftSchema);
2113
+ validatePublishedRecord(state, options.currentPublishedRecord);
2114
+ const validation = await options.validate?.(draftSchema);
2102
2115
  if (validation === false || Array.isArray(validation) && validation.length > 0) {
2103
2116
  return {
2104
2117
  success: false,
2105
2118
  error: { type: "validation_failed", issues: Array.isArray(validation) ? validation : [] }
2106
2119
  };
2107
2120
  }
2108
- const timestamp = options.timestamp ?? "1970-01-01T00:00:00.000Z";
2109
- if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("timestamp must be a valid date string.");
2121
+ const timestamp = transitionTimestamp(options);
2110
2122
  const archivedVersion = state.publishedVersion;
2111
- const archivedRecords = archivedVersion === void 0 ? [] : [
2123
+ const archivedRecords = options.currentPublishedRecord === void 0 ? [] : [
2112
2124
  {
2113
- formId: state.formId,
2114
- version: archivedVersion,
2125
+ ...options.currentPublishedRecord,
2115
2126
  status: "archived",
2116
- schema: { ...draftSchema, version: archivedVersion },
2117
- createdAt: timestamp,
2118
- publishedAt: timestamp,
2127
+ revision: options.currentPublishedRecord.revision + 1,
2119
2128
  archivedAt: timestamp
2120
2129
  }
2121
2130
  ];
@@ -2133,6 +2142,8 @@ function publishDraft(state, draftSchema, options = {}) {
2133
2142
  version: draftSchema.version,
2134
2143
  status: "published",
2135
2144
  schema: draftSchema,
2145
+ revision: 1,
2146
+ ...archivedVersion === void 0 ? {} : { createdFromVersion: archivedVersion },
2136
2147
  createdAt: timestamp,
2137
2148
  publishedAt: timestamp
2138
2149
  },
@@ -2141,6 +2152,43 @@ function publishDraft(state, draftSchema, options = {}) {
2141
2152
  }
2142
2153
  };
2143
2154
  }
2155
+ async function createPublishTransitionPlan(state, draftRecord, options = {}) {
2156
+ if (draftRecord.formId !== state.formId || draftRecord.version !== state.draftVersion || draftRecord.schema.id !== draftRecord.formId || draftRecord.schema.version !== draftRecord.version) {
2157
+ return { success: false, error: { type: "draft_not_found" } };
2158
+ }
2159
+ if (draftRecord.status !== "draft") {
2160
+ return { success: false, error: { type: "version_immutable", status: draftRecord.status } };
2161
+ }
2162
+ const expectedRevision = options.expectedRevision ?? state.revision;
2163
+ const timestamp = transitionTimestamp(options);
2164
+ const result = await publishDraft(state, draftRecord.schema, {
2165
+ ...options,
2166
+ expectedRevision,
2167
+ publishedAt: timestamp
2168
+ });
2169
+ if (!result.success) return result;
2170
+ const publishedRecordToSave = {
2171
+ ...draftRecord,
2172
+ status: "published",
2173
+ revision: draftRecord.revision + 1,
2174
+ publishedAt: timestamp
2175
+ };
2176
+ return {
2177
+ success: true,
2178
+ value: {
2179
+ nextState: result.value.nextState,
2180
+ plan: {
2181
+ formId: state.formId,
2182
+ expectedRevision,
2183
+ nextRevision: result.value.nextState.revision,
2184
+ draftToDeleteVersion: draftRecord.version,
2185
+ publishedRecordToSave,
2186
+ archivedRecordsToSave: result.value.archivedRecords,
2187
+ timestamp
2188
+ }
2189
+ }
2190
+ };
2191
+ }
2144
2192
  function deleteDraft(state, options = {}) {
2145
2193
  validateState(state);
2146
2194
  const conflict = revisionConflict(state, options.expectedRevision);
@@ -2170,6 +2218,7 @@ function assertVersionMutable(status) {
2170
2218
  calculatePageVisibility,
2171
2219
  cloneVersionToDraft,
2172
2220
  collectSchemaLocales,
2221
+ createPublishTransitionPlan,
2173
2222
  createResponseAccumulator,
2174
2223
  createSubmission,
2175
2224
  decodeSubmissionCursor,
package/dist/index.d.cts CHANGED
@@ -6,11 +6,13 @@ type Result<T, E> = {
6
6
  readonly error: E;
7
7
  };
8
8
  type FormVersionStatus = "draft" | "published" | "archived";
9
- interface FormVersionRecord {
9
+ interface FormVersionRecord extends ExtensibleNode {
10
10
  readonly formId: string;
11
11
  readonly version: number;
12
12
  readonly status: FormVersionStatus;
13
13
  readonly schema: FormSchema;
14
+ readonly revision: number;
15
+ readonly createdFromVersion?: number;
14
16
  readonly createdAt: string;
15
17
  readonly publishedAt?: string;
16
18
  readonly archivedAt?: string;
@@ -53,8 +55,10 @@ interface CloneVersionOptions {
53
55
  }
54
56
  interface PublishDraftOptions {
55
57
  readonly expectedRevision?: number;
56
- readonly validate?: (schema: FormSchema) => boolean | readonly SchemaIssue[];
57
- /** Supplies deterministic record timestamps while keeping the transition pure. */
58
+ readonly currentPublishedRecord?: FormVersionRecord;
59
+ readonly validate?: (schema: FormSchema) => boolean | Promise<boolean> | readonly SchemaIssue[] | Promise<readonly SchemaIssue[]>;
60
+ readonly publishedAt?: string;
61
+ /** @deprecated Use publishedAt. */
58
62
  readonly timestamp?: string;
59
63
  }
60
64
  interface DeleteDraftOptions {
@@ -71,7 +75,11 @@ declare function cloneVersionToDraft(state: FormVersionState, sourceSchema: Form
71
75
  readonly nextState: FormVersionState;
72
76
  readonly draftSchema: FormSchema;
73
77
  }, VersionTransitionError>;
74
- declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Result<PublishDraftResult, VersionTransitionError>;
78
+ declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Promise<Result<PublishDraftResult, VersionTransitionError>>;
79
+ declare function createPublishTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: PublishDraftOptions): Promise<Result<{
80
+ readonly nextState: FormVersionState;
81
+ readonly plan: VersionTransitionPlan;
82
+ }, VersionTransitionError>>;
75
83
  declare function deleteDraft(state: FormVersionState, options?: DeleteDraftOptions): Result<{
76
84
  readonly nextState: FormVersionState;
77
85
  }, VersionTransitionError>;
@@ -261,11 +269,14 @@ interface PagedSubmissionStorageAdapter extends FormStorageAdapter {
261
269
  listSubmissionPage(formId: string, options?: SubmissionPageQueryOptions): Promise<SubmissionPage>;
262
270
  }
263
271
  interface VersionTransitionPlan {
264
- readonly state: FormVersionState;
272
+ readonly formId: string;
265
273
  readonly expectedRevision: number;
266
- readonly draftToPublish?: FormSchema;
267
- readonly versionsToArchive: readonly number[];
268
- readonly versionsToDelete: readonly number[];
274
+ readonly nextRevision: number;
275
+ readonly draftToCreate?: FormVersionRecord;
276
+ readonly draftToDeleteVersion?: number;
277
+ readonly publishedRecordToSave?: FormVersionRecord;
278
+ readonly archivedRecordsToSave?: readonly FormVersionRecord[];
279
+ readonly timestamp: string;
269
280
  }
270
281
  interface VersionedFormStorageAdapter extends FormStorageAdapter {
271
282
  commitVersionTransition(plan: VersionTransitionPlan): Promise<{
@@ -377,7 +388,7 @@ interface CsvExportOptions {
377
388
  }
378
389
  interface CsvColumnDef {
379
390
  readonly header: string;
380
- readonly getValue: (context: CsvColumnContext) => string | number | boolean | null | undefined;
391
+ readonly getValue: (context: CsvColumnContext) => string | number | boolean | null | undefined | Promise<string | number | boolean | null | undefined>;
381
392
  }
382
393
  interface CsvColumnContext extends FormResponse {
383
394
  readonly submission: FormResponse;
@@ -509,4 +520,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
509
520
  declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
510
521
  declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
511
522
 
512
- export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createResponseAccumulator, createSubmission, decodeSubmissionCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
523
+ export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
package/dist/index.d.ts CHANGED
@@ -6,11 +6,13 @@ type Result<T, E> = {
6
6
  readonly error: E;
7
7
  };
8
8
  type FormVersionStatus = "draft" | "published" | "archived";
9
- interface FormVersionRecord {
9
+ interface FormVersionRecord extends ExtensibleNode {
10
10
  readonly formId: string;
11
11
  readonly version: number;
12
12
  readonly status: FormVersionStatus;
13
13
  readonly schema: FormSchema;
14
+ readonly revision: number;
15
+ readonly createdFromVersion?: number;
14
16
  readonly createdAt: string;
15
17
  readonly publishedAt?: string;
16
18
  readonly archivedAt?: string;
@@ -53,8 +55,10 @@ interface CloneVersionOptions {
53
55
  }
54
56
  interface PublishDraftOptions {
55
57
  readonly expectedRevision?: number;
56
- readonly validate?: (schema: FormSchema) => boolean | readonly SchemaIssue[];
57
- /** Supplies deterministic record timestamps while keeping the transition pure. */
58
+ readonly currentPublishedRecord?: FormVersionRecord;
59
+ readonly validate?: (schema: FormSchema) => boolean | Promise<boolean> | readonly SchemaIssue[] | Promise<readonly SchemaIssue[]>;
60
+ readonly publishedAt?: string;
61
+ /** @deprecated Use publishedAt. */
58
62
  readonly timestamp?: string;
59
63
  }
60
64
  interface DeleteDraftOptions {
@@ -71,7 +75,11 @@ declare function cloneVersionToDraft(state: FormVersionState, sourceSchema: Form
71
75
  readonly nextState: FormVersionState;
72
76
  readonly draftSchema: FormSchema;
73
77
  }, VersionTransitionError>;
74
- declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Result<PublishDraftResult, VersionTransitionError>;
78
+ declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Promise<Result<PublishDraftResult, VersionTransitionError>>;
79
+ declare function createPublishTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: PublishDraftOptions): Promise<Result<{
80
+ readonly nextState: FormVersionState;
81
+ readonly plan: VersionTransitionPlan;
82
+ }, VersionTransitionError>>;
75
83
  declare function deleteDraft(state: FormVersionState, options?: DeleteDraftOptions): Result<{
76
84
  readonly nextState: FormVersionState;
77
85
  }, VersionTransitionError>;
@@ -261,11 +269,14 @@ interface PagedSubmissionStorageAdapter extends FormStorageAdapter {
261
269
  listSubmissionPage(formId: string, options?: SubmissionPageQueryOptions): Promise<SubmissionPage>;
262
270
  }
263
271
  interface VersionTransitionPlan {
264
- readonly state: FormVersionState;
272
+ readonly formId: string;
265
273
  readonly expectedRevision: number;
266
- readonly draftToPublish?: FormSchema;
267
- readonly versionsToArchive: readonly number[];
268
- readonly versionsToDelete: readonly number[];
274
+ readonly nextRevision: number;
275
+ readonly draftToCreate?: FormVersionRecord;
276
+ readonly draftToDeleteVersion?: number;
277
+ readonly publishedRecordToSave?: FormVersionRecord;
278
+ readonly archivedRecordsToSave?: readonly FormVersionRecord[];
279
+ readonly timestamp: string;
269
280
  }
270
281
  interface VersionedFormStorageAdapter extends FormStorageAdapter {
271
282
  commitVersionTransition(plan: VersionTransitionPlan): Promise<{
@@ -377,7 +388,7 @@ interface CsvExportOptions {
377
388
  }
378
389
  interface CsvColumnDef {
379
390
  readonly header: string;
380
- readonly getValue: (context: CsvColumnContext) => string | number | boolean | null | undefined;
391
+ readonly getValue: (context: CsvColumnContext) => string | number | boolean | null | undefined | Promise<string | number | boolean | null | undefined>;
381
392
  }
382
393
  interface CsvColumnContext extends FormResponse {
383
394
  readonly submission: FormResponse;
@@ -509,4 +520,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
509
520
  declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
510
521
  declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
511
522
 
512
- export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createResponseAccumulator, createSubmission, decodeSubmissionCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
523
+ export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
package/dist/index.js CHANGED
@@ -1286,7 +1286,7 @@ async function* exportResponsesToCsvStream(schema, submissions, options = {}) {
1286
1286
  formVersion: response.formVersion ?? schema.version,
1287
1287
  schema
1288
1288
  };
1289
- const customCells = customColumns.map((column) => column.getValue(context));
1289
+ const customCells = await Promise.all(customColumns.map((column) => column.getValue(context)));
1290
1290
  yield `\r
1291
1291
  ${[...defaultCells, ...customCells].map((value) => escapeCsvCell(value, neutralizeFormulas)).join(",")}`;
1292
1292
  }
@@ -2031,31 +2031,39 @@ function cloneVersionToDraft(state, sourceSchema, options = {}) {
2031
2031
  }
2032
2032
  };
2033
2033
  }
2034
- function publishDraft(state, draftSchema, options = {}) {
2034
+ function validatePublishedRecord(state, record) {
2035
+ if (record === void 0) return;
2036
+ if (record.formId !== state.formId || record.version !== state.publishedVersion || record.status !== "published" || record.schema.id !== record.formId || record.schema.version !== record.version) {
2037
+ throw new TypeError("currentPublishedRecord must match the state's published version.");
2038
+ }
2039
+ }
2040
+ function transitionTimestamp(options) {
2041
+ const timestamp = options.publishedAt ?? options.timestamp ?? "1970-01-01T00:00:00.000Z";
2042
+ if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("publishedAt must be a valid date string.");
2043
+ return timestamp;
2044
+ }
2045
+ async function publishDraft(state, draftSchema, options = {}) {
2035
2046
  validateState(state);
2036
2047
  const conflict = revisionConflict(state, options.expectedRevision);
2037
2048
  if (conflict !== void 0) return conflict;
2038
2049
  if (state.draftVersion === void 0 || draftSchema.id !== state.formId || draftSchema.version !== state.draftVersion) {
2039
2050
  return { success: false, error: { type: "draft_not_found" } };
2040
2051
  }
2041
- const validation = options.validate?.(draftSchema);
2052
+ validatePublishedRecord(state, options.currentPublishedRecord);
2053
+ const validation = await options.validate?.(draftSchema);
2042
2054
  if (validation === false || Array.isArray(validation) && validation.length > 0) {
2043
2055
  return {
2044
2056
  success: false,
2045
2057
  error: { type: "validation_failed", issues: Array.isArray(validation) ? validation : [] }
2046
2058
  };
2047
2059
  }
2048
- const timestamp = options.timestamp ?? "1970-01-01T00:00:00.000Z";
2049
- if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("timestamp must be a valid date string.");
2060
+ const timestamp = transitionTimestamp(options);
2050
2061
  const archivedVersion = state.publishedVersion;
2051
- const archivedRecords = archivedVersion === void 0 ? [] : [
2062
+ const archivedRecords = options.currentPublishedRecord === void 0 ? [] : [
2052
2063
  {
2053
- formId: state.formId,
2054
- version: archivedVersion,
2064
+ ...options.currentPublishedRecord,
2055
2065
  status: "archived",
2056
- schema: { ...draftSchema, version: archivedVersion },
2057
- createdAt: timestamp,
2058
- publishedAt: timestamp,
2066
+ revision: options.currentPublishedRecord.revision + 1,
2059
2067
  archivedAt: timestamp
2060
2068
  }
2061
2069
  ];
@@ -2073,6 +2081,8 @@ function publishDraft(state, draftSchema, options = {}) {
2073
2081
  version: draftSchema.version,
2074
2082
  status: "published",
2075
2083
  schema: draftSchema,
2084
+ revision: 1,
2085
+ ...archivedVersion === void 0 ? {} : { createdFromVersion: archivedVersion },
2076
2086
  createdAt: timestamp,
2077
2087
  publishedAt: timestamp
2078
2088
  },
@@ -2081,6 +2091,43 @@ function publishDraft(state, draftSchema, options = {}) {
2081
2091
  }
2082
2092
  };
2083
2093
  }
2094
+ async function createPublishTransitionPlan(state, draftRecord, options = {}) {
2095
+ if (draftRecord.formId !== state.formId || draftRecord.version !== state.draftVersion || draftRecord.schema.id !== draftRecord.formId || draftRecord.schema.version !== draftRecord.version) {
2096
+ return { success: false, error: { type: "draft_not_found" } };
2097
+ }
2098
+ if (draftRecord.status !== "draft") {
2099
+ return { success: false, error: { type: "version_immutable", status: draftRecord.status } };
2100
+ }
2101
+ const expectedRevision = options.expectedRevision ?? state.revision;
2102
+ const timestamp = transitionTimestamp(options);
2103
+ const result = await publishDraft(state, draftRecord.schema, {
2104
+ ...options,
2105
+ expectedRevision,
2106
+ publishedAt: timestamp
2107
+ });
2108
+ if (!result.success) return result;
2109
+ const publishedRecordToSave = {
2110
+ ...draftRecord,
2111
+ status: "published",
2112
+ revision: draftRecord.revision + 1,
2113
+ publishedAt: timestamp
2114
+ };
2115
+ return {
2116
+ success: true,
2117
+ value: {
2118
+ nextState: result.value.nextState,
2119
+ plan: {
2120
+ formId: state.formId,
2121
+ expectedRevision,
2122
+ nextRevision: result.value.nextState.revision,
2123
+ draftToDeleteVersion: draftRecord.version,
2124
+ publishedRecordToSave,
2125
+ archivedRecordsToSave: result.value.archivedRecords,
2126
+ timestamp
2127
+ }
2128
+ }
2129
+ };
2130
+ }
2084
2131
  function deleteDraft(state, options = {}) {
2085
2132
  validateState(state);
2086
2133
  const conflict = revisionConflict(state, options.expectedRevision);
@@ -2109,6 +2156,7 @@ export {
2109
2156
  calculatePageVisibility,
2110
2157
  cloneVersionToDraft,
2111
2158
  collectSchemaLocales,
2159
+ createPublishTransitionPlan,
2112
2160
  createResponseAccumulator,
2113
2161
  createSubmission,
2114
2162
  decodeSubmissionCursor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/core",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },