@form-engine-ts/storage-mongodb 4.1.0 → 4.3.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
@@ -25,7 +25,11 @@ const submissions = await storage.listSubmissions(schema.id, schema.version, {
25
25
  });
26
26
  ```
27
27
 
28
- The caller owns the MongoDB connection lifecycle. Collection names can be customized in the factory options. `createIndexes()` creates named indexes for form/timestamp range scans and locale queries. Range boundaries are inclusive, and MongoDB sorts by submission time and then ID. `clearResponses(formId)` preserves schemas and other forms; `clear()` removes documents only from the configured collections and does not drop collections or indexes.
28
+ The caller owns the MongoDB connection lifecycle. Collection names can be customized with the legacy flat options or
29
+ with `collectionNames`. `customIndexes` adds application-owned indexes to the configured forms, versions, or response
30
+ collections; `createIndexes()` creates the built-in indexes first and then the custom definitions. Range boundaries are
31
+ inclusive, and MongoDB sorts by submission time and then ID. `clearResponses(formId)` preserves schemas and other forms;
32
+ `clear()` removes documents only from the configured collections and does not drop collections or indexes.
29
33
 
30
34
  `listSubmissionPage(formId, { version, pageSize, cursor, since, until, locale })` performs bounded reads using the
31
35
  compound `submittedAt`/response-ID cursor. Run `createIndexes()` after upgrading to create the matching compound index.
package/dist/index.cjs CHANGED
@@ -148,23 +148,56 @@ function collectionName(value, fallback, optionName) {
148
148
  if (value.trim().length === 0) throw new TypeError(`${optionName} must be a non-empty string.`);
149
149
  return value;
150
150
  }
151
+ function normalizeIndexSpecification(spec) {
152
+ if (typeof spec === "string") return { [spec]: 1 };
153
+ if (Array.isArray(spec)) return Object.fromEntries(spec.map((field) => [field, 1]));
154
+ if (spec instanceof Map) return spec;
155
+ if (!isRecord(spec) || Object.keys(spec).length === 0) throw new TypeError("Index specification must not be empty.");
156
+ return spec;
157
+ }
158
+ function customIndexDescriptions(definitions) {
159
+ return (definitions ?? []).map(({ spec, options: indexOptions }) => ({
160
+ ...indexOptions ?? {},
161
+ key: normalizeIndexSpecification(spec)
162
+ }));
163
+ }
164
+ async function createConfiguredIndexes(collection, defaults, custom) {
165
+ const definitions = custom ?? [];
166
+ if (definitions.length === 0) {
167
+ await collection.createIndexes(defaults);
168
+ return;
169
+ }
170
+ if (collection.createIndex === void 0) {
171
+ await collection.createIndexes([...defaults, ...customIndexDescriptions(definitions)]);
172
+ return;
173
+ }
174
+ await collection.createIndexes(defaults);
175
+ for (const { spec, options: indexOptions } of definitions) {
176
+ await collection.createIndex(normalizeIndexSpecification(spec), indexOptions);
177
+ }
178
+ }
151
179
  function createMongoDbStorage(options) {
152
180
  if (options?.db === void 0) throw new TypeError("db is required.");
153
- const schemasCollectionName = collectionName(options.schemasCollectionName, "form_schemas", "schemasCollectionName");
181
+ const collectionNames = options.collectionNames;
182
+ const schemasCollectionName = collectionName(
183
+ collectionNames?.forms ?? options.schemasCollectionName,
184
+ collectionNames === void 0 ? "form_schemas" : "forms",
185
+ collectionNames?.forms === void 0 ? "schemasCollectionName" : "collectionNames.forms"
186
+ );
154
187
  const responsesCollectionName = collectionName(
155
- options.responsesCollectionName,
188
+ collectionNames?.formResponses ?? options.responsesCollectionName,
156
189
  "form_responses",
157
- "responsesCollectionName"
190
+ collectionNames?.formResponses === void 0 ? "responsesCollectionName" : "collectionNames.formResponses"
158
191
  );
159
192
  const versionsCollectionName = collectionName(
160
- options.versionsCollectionName,
193
+ collectionNames?.formVersions ?? options.versionsCollectionName,
161
194
  "form_versions",
162
- "versionsCollectionName"
195
+ collectionNames?.formVersions === void 0 ? "versionsCollectionName" : "collectionNames.formVersions"
163
196
  );
164
197
  const versionStatesCollectionName = collectionName(
165
- options.versionStatesCollectionName,
198
+ collectionNames?.formVersionStates ?? options.versionStatesCollectionName,
166
199
  "form_version_states",
167
- "versionStatesCollectionName"
200
+ collectionNames?.formVersionStates === void 0 ? "versionStatesCollectionName" : "collectionNames.formVersionStates"
168
201
  );
169
202
  const versionEventsCollectionName = "form_version_events";
170
203
  const schemas = options.db.collection(schemasCollectionName);
@@ -481,28 +514,37 @@ function createMongoDbStorage(options) {
481
514
  },
482
515
  async createIndexes() {
483
516
  await Promise.all([
484
- submissions.createIndexes([
485
- { key: { formId: 1, submittedAt: 1, _id: 1 }, name: "form_responses_form_submitted_at_id" },
486
- { key: { "submission.locale": 1 }, name: "form_responses_locale" }
487
- ]),
488
- versions.createIndexes([
489
- { key: { formId: 1, version: 1 }, name: "unique_version_per_form", unique: true },
490
- {
491
- key: { formId: 1 },
492
- name: "unique_draft_per_form",
493
- unique: true,
494
- partialFilterExpression: { status: "draft" }
495
- },
496
- {
497
- key: { formId: 1 },
498
- name: "unique_published_per_form",
499
- unique: true,
500
- partialFilterExpression: { status: "published" }
501
- }
502
- ]),
517
+ createConfiguredIndexes(
518
+ submissions,
519
+ [
520
+ { key: { formId: 1, submittedAt: 1, _id: 1 }, name: "form_responses_form_submitted_at_id" },
521
+ { key: { "submission.locale": 1 }, name: "form_responses_locale" }
522
+ ],
523
+ options.customIndexes?.formResponses
524
+ ),
525
+ createConfiguredIndexes(
526
+ versions,
527
+ [
528
+ { key: { formId: 1, version: 1 }, name: "unique_version_per_form", unique: true },
529
+ {
530
+ key: { formId: 1 },
531
+ name: "unique_draft_per_form",
532
+ unique: true,
533
+ partialFilterExpression: { status: "draft" }
534
+ },
535
+ {
536
+ key: { formId: 1 },
537
+ name: "unique_published_per_form",
538
+ unique: true,
539
+ partialFilterExpression: { status: "published" }
540
+ }
541
+ ],
542
+ options.customIndexes?.formVersions
543
+ ),
503
544
  versionEvents.createIndexes([
504
545
  { key: { formId: 1, fromRevision: 1, eventIndex: 1 }, name: "form_version_events_revision" }
505
- ])
546
+ ]),
547
+ createConfiguredIndexes(schemas, [], options.customIndexes?.forms)
506
548
  ]);
507
549
  }
508
550
  };
package/dist/index.d.cts CHANGED
@@ -1,12 +1,27 @@
1
1
  import { PagedSubmissionStorageAdapter, VersionedFormStorageAdapter, SubmissionFilter } from '@form-engine-ts/core';
2
- import { Db, Document } from 'mongodb';
2
+ import { IndexSpecification, CreateIndexesOptions, Db, Document } from 'mongodb';
3
3
 
4
+ interface MongoCustomIndexDefinition {
5
+ readonly spec: IndexSpecification;
6
+ readonly options?: CreateIndexesOptions;
7
+ }
4
8
  interface MongoDbStorageOptions {
5
9
  readonly db: Db;
6
10
  readonly schemasCollectionName?: string;
7
11
  readonly responsesCollectionName?: string;
8
12
  readonly versionsCollectionName?: string;
9
13
  readonly versionStatesCollectionName?: string;
14
+ readonly collectionNames?: {
15
+ readonly forms?: string;
16
+ readonly formVersions?: string;
17
+ readonly formVersionStates?: string;
18
+ readonly formResponses?: string;
19
+ };
20
+ readonly customIndexes?: {
21
+ readonly forms?: readonly MongoCustomIndexDefinition[];
22
+ readonly formVersions?: readonly MongoCustomIndexDefinition[];
23
+ readonly formResponses?: readonly MongoCustomIndexDefinition[];
24
+ };
10
25
  }
11
26
  interface MongoDbStorageAdapter extends PagedSubmissionStorageAdapter, VersionedFormStorageAdapter {
12
27
  createIndexes(): Promise<void>;
@@ -14,4 +29,4 @@ interface MongoDbStorageAdapter extends PagedSubmissionStorageAdapter, Versioned
14
29
  declare function submissionFilterToMongo(filter: SubmissionFilter): Document;
15
30
  declare function createMongoDbStorage(options: MongoDbStorageOptions): MongoDbStorageAdapter;
16
31
 
17
- export { type MongoDbStorageAdapter, type MongoDbStorageOptions, createMongoDbStorage, submissionFilterToMongo };
32
+ export { type MongoCustomIndexDefinition, type MongoDbStorageAdapter, type MongoDbStorageOptions, createMongoDbStorage, submissionFilterToMongo };
package/dist/index.d.ts CHANGED
@@ -1,12 +1,27 @@
1
1
  import { PagedSubmissionStorageAdapter, VersionedFormStorageAdapter, SubmissionFilter } from '@form-engine-ts/core';
2
- import { Db, Document } from 'mongodb';
2
+ import { IndexSpecification, CreateIndexesOptions, Db, Document } from 'mongodb';
3
3
 
4
+ interface MongoCustomIndexDefinition {
5
+ readonly spec: IndexSpecification;
6
+ readonly options?: CreateIndexesOptions;
7
+ }
4
8
  interface MongoDbStorageOptions {
5
9
  readonly db: Db;
6
10
  readonly schemasCollectionName?: string;
7
11
  readonly responsesCollectionName?: string;
8
12
  readonly versionsCollectionName?: string;
9
13
  readonly versionStatesCollectionName?: string;
14
+ readonly collectionNames?: {
15
+ readonly forms?: string;
16
+ readonly formVersions?: string;
17
+ readonly formVersionStates?: string;
18
+ readonly formResponses?: string;
19
+ };
20
+ readonly customIndexes?: {
21
+ readonly forms?: readonly MongoCustomIndexDefinition[];
22
+ readonly formVersions?: readonly MongoCustomIndexDefinition[];
23
+ readonly formResponses?: readonly MongoCustomIndexDefinition[];
24
+ };
10
25
  }
11
26
  interface MongoDbStorageAdapter extends PagedSubmissionStorageAdapter, VersionedFormStorageAdapter {
12
27
  createIndexes(): Promise<void>;
@@ -14,4 +29,4 @@ interface MongoDbStorageAdapter extends PagedSubmissionStorageAdapter, Versioned
14
29
  declare function submissionFilterToMongo(filter: SubmissionFilter): Document;
15
30
  declare function createMongoDbStorage(options: MongoDbStorageOptions): MongoDbStorageAdapter;
16
31
 
17
- export { type MongoDbStorageAdapter, type MongoDbStorageOptions, createMongoDbStorage, submissionFilterToMongo };
32
+ export { type MongoCustomIndexDefinition, type MongoDbStorageAdapter, type MongoDbStorageOptions, createMongoDbStorage, submissionFilterToMongo };
package/dist/index.js CHANGED
@@ -131,23 +131,56 @@ function collectionName(value, fallback, optionName) {
131
131
  if (value.trim().length === 0) throw new TypeError(`${optionName} must be a non-empty string.`);
132
132
  return value;
133
133
  }
134
+ function normalizeIndexSpecification(spec) {
135
+ if (typeof spec === "string") return { [spec]: 1 };
136
+ if (Array.isArray(spec)) return Object.fromEntries(spec.map((field) => [field, 1]));
137
+ if (spec instanceof Map) return spec;
138
+ if (!isRecord(spec) || Object.keys(spec).length === 0) throw new TypeError("Index specification must not be empty.");
139
+ return spec;
140
+ }
141
+ function customIndexDescriptions(definitions) {
142
+ return (definitions ?? []).map(({ spec, options: indexOptions }) => ({
143
+ ...indexOptions ?? {},
144
+ key: normalizeIndexSpecification(spec)
145
+ }));
146
+ }
147
+ async function createConfiguredIndexes(collection, defaults, custom) {
148
+ const definitions = custom ?? [];
149
+ if (definitions.length === 0) {
150
+ await collection.createIndexes(defaults);
151
+ return;
152
+ }
153
+ if (collection.createIndex === void 0) {
154
+ await collection.createIndexes([...defaults, ...customIndexDescriptions(definitions)]);
155
+ return;
156
+ }
157
+ await collection.createIndexes(defaults);
158
+ for (const { spec, options: indexOptions } of definitions) {
159
+ await collection.createIndex(normalizeIndexSpecification(spec), indexOptions);
160
+ }
161
+ }
134
162
  function createMongoDbStorage(options) {
135
163
  if (options?.db === void 0) throw new TypeError("db is required.");
136
- const schemasCollectionName = collectionName(options.schemasCollectionName, "form_schemas", "schemasCollectionName");
164
+ const collectionNames = options.collectionNames;
165
+ const schemasCollectionName = collectionName(
166
+ collectionNames?.forms ?? options.schemasCollectionName,
167
+ collectionNames === void 0 ? "form_schemas" : "forms",
168
+ collectionNames?.forms === void 0 ? "schemasCollectionName" : "collectionNames.forms"
169
+ );
137
170
  const responsesCollectionName = collectionName(
138
- options.responsesCollectionName,
171
+ collectionNames?.formResponses ?? options.responsesCollectionName,
139
172
  "form_responses",
140
- "responsesCollectionName"
173
+ collectionNames?.formResponses === void 0 ? "responsesCollectionName" : "collectionNames.formResponses"
141
174
  );
142
175
  const versionsCollectionName = collectionName(
143
- options.versionsCollectionName,
176
+ collectionNames?.formVersions ?? options.versionsCollectionName,
144
177
  "form_versions",
145
- "versionsCollectionName"
178
+ collectionNames?.formVersions === void 0 ? "versionsCollectionName" : "collectionNames.formVersions"
146
179
  );
147
180
  const versionStatesCollectionName = collectionName(
148
- options.versionStatesCollectionName,
181
+ collectionNames?.formVersionStates ?? options.versionStatesCollectionName,
149
182
  "form_version_states",
150
- "versionStatesCollectionName"
183
+ collectionNames?.formVersionStates === void 0 ? "versionStatesCollectionName" : "collectionNames.formVersionStates"
151
184
  );
152
185
  const versionEventsCollectionName = "form_version_events";
153
186
  const schemas = options.db.collection(schemasCollectionName);
@@ -464,28 +497,37 @@ function createMongoDbStorage(options) {
464
497
  },
465
498
  async createIndexes() {
466
499
  await Promise.all([
467
- submissions.createIndexes([
468
- { key: { formId: 1, submittedAt: 1, _id: 1 }, name: "form_responses_form_submitted_at_id" },
469
- { key: { "submission.locale": 1 }, name: "form_responses_locale" }
470
- ]),
471
- versions.createIndexes([
472
- { key: { formId: 1, version: 1 }, name: "unique_version_per_form", unique: true },
473
- {
474
- key: { formId: 1 },
475
- name: "unique_draft_per_form",
476
- unique: true,
477
- partialFilterExpression: { status: "draft" }
478
- },
479
- {
480
- key: { formId: 1 },
481
- name: "unique_published_per_form",
482
- unique: true,
483
- partialFilterExpression: { status: "published" }
484
- }
485
- ]),
500
+ createConfiguredIndexes(
501
+ submissions,
502
+ [
503
+ { key: { formId: 1, submittedAt: 1, _id: 1 }, name: "form_responses_form_submitted_at_id" },
504
+ { key: { "submission.locale": 1 }, name: "form_responses_locale" }
505
+ ],
506
+ options.customIndexes?.formResponses
507
+ ),
508
+ createConfiguredIndexes(
509
+ versions,
510
+ [
511
+ { key: { formId: 1, version: 1 }, name: "unique_version_per_form", unique: true },
512
+ {
513
+ key: { formId: 1 },
514
+ name: "unique_draft_per_form",
515
+ unique: true,
516
+ partialFilterExpression: { status: "draft" }
517
+ },
518
+ {
519
+ key: { formId: 1 },
520
+ name: "unique_published_per_form",
521
+ unique: true,
522
+ partialFilterExpression: { status: "published" }
523
+ }
524
+ ],
525
+ options.customIndexes?.formVersions
526
+ ),
486
527
  versionEvents.createIndexes([
487
528
  { key: { formId: 1, fromRevision: 1, eventIndex: 1 }, name: "form_version_events_revision" }
488
- ])
529
+ ]),
530
+ createConfiguredIndexes(schemas, [], options.customIndexes?.forms)
489
531
  ]);
490
532
  }
491
533
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/storage-mongodb",
3
- "version": "4.1.0",
3
+ "version": "4.3.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -39,7 +39,7 @@
39
39
  "typescript"
40
40
  ],
41
41
  "dependencies": {
42
- "@form-engine-ts/core": "4.1.0"
42
+ "@form-engine-ts/core": "4.3.1"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "mongodb": "^6.0.0"