@puzzle-ui/storage-mongodb 0.0.0-stage → 0.5.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 +17 -2
- package/dist/LICENSE +21 -0
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/mongodb.adapter.cjs +320 -0
- package/dist/mongodb.adapter.cjs.map +1 -0
- package/dist/mongodb.adapter.d.cts +25 -0
- package/dist/mongodb.adapter.d.ts +25 -0
- package/dist/mongodb.adapter.js +320 -0
- package/dist/mongodb.adapter.js.map +1 -0
- package/dist/mongodb.migrations.cjs +159 -0
- package/dist/mongodb.migrations.cjs.map +1 -0
- package/dist/mongodb.migrations.d.cts +18 -0
- package/dist/mongodb.migrations.d.ts +18 -0
- package/dist/mongodb.migrations.js +159 -0
- package/dist/mongodb.migrations.js.map +1 -0
- package/dist/mongodb.types.d.cts +12 -0
- package/dist/mongodb.types.d.ts +12 -0
- package/package.json +47 -4
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import "mongodb";
|
|
2
|
+
import { StorageAtomicityError, StorageConflictError, StorageDuplicateError, StorageValidationError, validateQuery } from "@puzzle-ui/storage";
|
|
3
|
+
// src/mongodb.adapter.ts
|
|
4
|
+
const isDuplicateError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === 11e3;
|
|
5
|
+
const mapMongoError = (error) => {
|
|
6
|
+
if (isDuplicateError(error)) throw new StorageDuplicateError("mongodb_unique_constraint");
|
|
7
|
+
throw error;
|
|
8
|
+
};
|
|
9
|
+
const toMongoField = (field) => field === "id" ? "_id" : field;
|
|
10
|
+
const encodeCursor = (payload) => ({ value: Buffer.from(JSON.stringify(payload)).toString("base64url") });
|
|
11
|
+
const decodeCursor = (cursor) => {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(Buffer.from(cursor.value, "base64url").toString("utf8"));
|
|
14
|
+
} catch {
|
|
15
|
+
throw new StorageValidationError("Invalid MongoDB cursor.");
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const toRecord = (document) => {
|
|
19
|
+
if (!document) return null;
|
|
20
|
+
const { _id, ...data } = document;
|
|
21
|
+
return {
|
|
22
|
+
...data,
|
|
23
|
+
id: String(_id)
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const toDocument = (record) => {
|
|
27
|
+
const { id, createdAt, updatedAt, revision, ...data } = record;
|
|
28
|
+
const now = /* @__PURE__ */ new Date();
|
|
29
|
+
return {
|
|
30
|
+
...data,
|
|
31
|
+
_id: id,
|
|
32
|
+
createdAt: createdAt ?? now,
|
|
33
|
+
updatedAt: updatedAt ?? now,
|
|
34
|
+
revision: revision ?? 1
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const toMongoWhere = (where) => {
|
|
38
|
+
if (!where) return {};
|
|
39
|
+
const result = [];
|
|
40
|
+
for (const [field, comparison] of Object.entries(where)) {
|
|
41
|
+
if (field === "and" || field === "or") {
|
|
42
|
+
const nested = comparison.map((item) => toMongoWhere(item));
|
|
43
|
+
result.push({ [field === "and" ? "$and" : "$or"]: nested });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const value = comparison;
|
|
47
|
+
if (typeof value !== "object" || value === null || !("operator" in value) || typeof value.operator !== "string" || !("value" in value)) throw new StorageValidationError(`Invalid query comparison for field: ${field}`);
|
|
48
|
+
const mongoField = toMongoField(field);
|
|
49
|
+
const operator = {
|
|
50
|
+
eq: "$eq",
|
|
51
|
+
ne: "$ne",
|
|
52
|
+
in: "$in",
|
|
53
|
+
notIn: "$nin",
|
|
54
|
+
gt: "$gt",
|
|
55
|
+
gte: "$gte",
|
|
56
|
+
lt: "$lt",
|
|
57
|
+
lte: "$lte",
|
|
58
|
+
exists: "$exists"
|
|
59
|
+
}[value.operator];
|
|
60
|
+
if (!operator) throw new StorageValidationError(`Unsupported query operator: ${value.operator}`);
|
|
61
|
+
result.push({ [mongoField]: { [operator]: value.value } });
|
|
62
|
+
}
|
|
63
|
+
if (result.length === 0) return {};
|
|
64
|
+
if (result.length === 1) return result[0];
|
|
65
|
+
return { $and: result };
|
|
66
|
+
};
|
|
67
|
+
const toMongoSort = (orderBy) => {
|
|
68
|
+
const sort = {};
|
|
69
|
+
for (const item of orderBy ?? []) sort[toMongoField(String(item.field))] = item.direction === "asc" ? 1 : -1;
|
|
70
|
+
if (!("_id" in sort)) sort._id = 1;
|
|
71
|
+
return sort;
|
|
72
|
+
};
|
|
73
|
+
const toCursorFilter = (orderBy, values) => {
|
|
74
|
+
const fields = [...orderBy.map((item) => ({
|
|
75
|
+
field: toMongoField(String(item.field)),
|
|
76
|
+
direction: item.direction
|
|
77
|
+
}))];
|
|
78
|
+
if (!fields.some((item) => item.field === "_id")) fields.push({
|
|
79
|
+
field: "_id",
|
|
80
|
+
direction: "asc"
|
|
81
|
+
});
|
|
82
|
+
const branches = [];
|
|
83
|
+
for (let index = 0; index < fields.length; index++) {
|
|
84
|
+
const prefix = [];
|
|
85
|
+
for (let previous = 0; previous < index; previous++) prefix.push({ [fields[previous].field]: values[previous] });
|
|
86
|
+
const field = fields[index];
|
|
87
|
+
const operator = field.direction === "asc" ? "$gt" : "$lt";
|
|
88
|
+
branches.push({ $and: [...prefix, { [field.field]: { [operator]: values[index] } }] });
|
|
89
|
+
}
|
|
90
|
+
return { $or: branches };
|
|
91
|
+
};
|
|
92
|
+
const buildCondition = (condition) => {
|
|
93
|
+
if (!condition) return {};
|
|
94
|
+
const result = {};
|
|
95
|
+
if (condition.expectedRevision !== void 0) result.revision = condition.expectedRevision;
|
|
96
|
+
if (condition.expectedUpdatedAt !== void 0) result.updatedAt = condition.expectedUpdatedAt;
|
|
97
|
+
return result;
|
|
98
|
+
};
|
|
99
|
+
const cleanChanges = (changes) => {
|
|
100
|
+
const result = { ...changes };
|
|
101
|
+
delete result.id;
|
|
102
|
+
delete result.createdAt;
|
|
103
|
+
delete result.updatedAt;
|
|
104
|
+
delete result.revision;
|
|
105
|
+
return result;
|
|
106
|
+
};
|
|
107
|
+
var MongoStorageAdapter = class {
|
|
108
|
+
capabilities;
|
|
109
|
+
database;
|
|
110
|
+
client;
|
|
111
|
+
prefix;
|
|
112
|
+
constructor(options) {
|
|
113
|
+
this.database = options.database;
|
|
114
|
+
this.client = options.client;
|
|
115
|
+
this.prefix = options.collectionPrefix ?? "puzzle_";
|
|
116
|
+
const transactionsEnabled = options.transactionsEnabled === true && !!options.client;
|
|
117
|
+
this.capabilities = {
|
|
118
|
+
transactions: transactionsEnabled,
|
|
119
|
+
atomicBatch: transactionsEnabled,
|
|
120
|
+
conditionalWrites: true,
|
|
121
|
+
uniqueConstraints: true,
|
|
122
|
+
upsert: true,
|
|
123
|
+
cursorPagination: true,
|
|
124
|
+
migrations: true,
|
|
125
|
+
ttlIndexes: true
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
getCapabilities() {
|
|
129
|
+
const limitations = this.capabilities.transactions ? [] : ["Multi-document transactions are disabled. Configure a replica set or sharded deployment and set transactionsEnabled to true."];
|
|
130
|
+
return {
|
|
131
|
+
capabilities: this.capabilities,
|
|
132
|
+
limitations
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
collection(name) {
|
|
136
|
+
return this.database.collection(`${this.prefix}${name}`);
|
|
137
|
+
}
|
|
138
|
+
async registerCollection(definition) {
|
|
139
|
+
const collectionName = `${this.prefix}${definition.name}`;
|
|
140
|
+
try {
|
|
141
|
+
await this.database.createCollection(collectionName);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (!(typeof error === "object" && error !== null && "code" in error && error.code === 48)) throw error;
|
|
144
|
+
}
|
|
145
|
+
const collection = this.database.collection(collectionName);
|
|
146
|
+
const indexes = (definition.indexes ?? []).map((index) => ({
|
|
147
|
+
key: Object.fromEntries(index.fields.map((field) => [toMongoField(String(field)), 1])),
|
|
148
|
+
name: index.name,
|
|
149
|
+
unique: index.unique ?? false,
|
|
150
|
+
...index.expireAfterSeconds !== void 0 ? { expireAfterSeconds: index.expireAfterSeconds } : {}
|
|
151
|
+
}));
|
|
152
|
+
if (indexes.length > 0) await collection.createIndexes(indexes);
|
|
153
|
+
}
|
|
154
|
+
async findById(collection, id) {
|
|
155
|
+
const document = await this.collection(collection).findOne({ _id: id });
|
|
156
|
+
return toRecord(document);
|
|
157
|
+
}
|
|
158
|
+
async findOne(collection, query) {
|
|
159
|
+
validateQuery(query);
|
|
160
|
+
const document = await this.collection(collection).findOne(toMongoWhere(query.where), {
|
|
161
|
+
sort: toMongoSort(query.orderBy),
|
|
162
|
+
...query.offset !== void 0 ? { skip: query.offset } : {}
|
|
163
|
+
});
|
|
164
|
+
return toRecord(document);
|
|
165
|
+
}
|
|
166
|
+
async findMany(collection, query = {}) {
|
|
167
|
+
validateQuery(query);
|
|
168
|
+
return (await this.collection(collection).find(toMongoWhere(query.where)).sort(toMongoSort(query.orderBy)).skip(query.offset ?? 0).limit(query.limit ?? 0).toArray()).map((document) => toRecord(document));
|
|
169
|
+
}
|
|
170
|
+
async findPage(collection, query) {
|
|
171
|
+
if (!Number.isInteger(query.first) || query.first < 1) throw new StorageValidationError("Cursor page size must be a positive integer.");
|
|
172
|
+
const orderBy = query.orderBy ?? [{
|
|
173
|
+
field: "id",
|
|
174
|
+
direction: "asc"
|
|
175
|
+
}];
|
|
176
|
+
let filter = toMongoWhere(query.where);
|
|
177
|
+
if (query.after) {
|
|
178
|
+
const payload = decodeCursor(query.after);
|
|
179
|
+
const cursorFilter = toCursorFilter(orderBy, payload.values);
|
|
180
|
+
filter = Object.keys(filter).length ? { $and: [filter, cursorFilter] } : cursorFilter;
|
|
181
|
+
}
|
|
182
|
+
const sort = toMongoSort(orderBy);
|
|
183
|
+
const documents = await this.collection(collection).find(filter).sort(sort).limit(query.first + 1).toArray();
|
|
184
|
+
const hasNextPage = documents.length > query.first;
|
|
185
|
+
const items = documents.slice(0, query.first);
|
|
186
|
+
const last = items.at(-1);
|
|
187
|
+
const cursorFields = [...orderBy.map((item) => toMongoField(String(item.field)))];
|
|
188
|
+
if (!cursorFields.includes("_id")) cursorFields.push("_id");
|
|
189
|
+
const nextCursor = hasNextPage && last ? encodeCursor({ values: cursorFields.map((field) => field === "_id" ? last._id : last[field]) }) : null;
|
|
190
|
+
return {
|
|
191
|
+
items: items.map((item) => toRecord(item)),
|
|
192
|
+
hasNextPage,
|
|
193
|
+
nextCursor
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async insert(collection, record) {
|
|
197
|
+
const document = toDocument(record);
|
|
198
|
+
try {
|
|
199
|
+
await this.collection(collection).insertOne(document);
|
|
200
|
+
return toRecord(document);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return mapMongoError(error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async upsert(collection, query, record) {
|
|
206
|
+
validateQuery(query);
|
|
207
|
+
const existing = await this.findOne(collection, query);
|
|
208
|
+
if (existing) return await this.update(collection, existing.id, record) ?? existing;
|
|
209
|
+
return this.insert(collection, record);
|
|
210
|
+
}
|
|
211
|
+
async update(collection, id, changes, condition) {
|
|
212
|
+
const target = this.collection(collection);
|
|
213
|
+
const filter = {
|
|
214
|
+
_id: id,
|
|
215
|
+
...buildCondition(condition)
|
|
216
|
+
};
|
|
217
|
+
const update = {
|
|
218
|
+
$set: {
|
|
219
|
+
...cleanChanges(changes),
|
|
220
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
221
|
+
},
|
|
222
|
+
$inc: { revision: 1 }
|
|
223
|
+
};
|
|
224
|
+
if ((await target.updateOne(filter, update)).matchedCount === 0) {
|
|
225
|
+
if (condition && await target.findOne({ _id: id })) throw new StorageConflictError();
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return toRecord(await target.findOne({ _id: id }));
|
|
229
|
+
}
|
|
230
|
+
async updateMany(collection, query, changes) {
|
|
231
|
+
validateQuery(query);
|
|
232
|
+
return (await this.collection(collection).updateMany(toMongoWhere(query.where), {
|
|
233
|
+
$set: {
|
|
234
|
+
...cleanChanges(changes),
|
|
235
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
236
|
+
},
|
|
237
|
+
$inc: { revision: 1 }
|
|
238
|
+
})).modifiedCount;
|
|
239
|
+
}
|
|
240
|
+
async delete(collection, id, condition) {
|
|
241
|
+
const target = this.collection(collection);
|
|
242
|
+
if ((await target.deleteOne({
|
|
243
|
+
_id: id,
|
|
244
|
+
...buildCondition(condition)
|
|
245
|
+
})).deletedCount > 0) return true;
|
|
246
|
+
if (condition && await target.findOne({ _id: id })) throw new StorageConflictError();
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
async deleteMany(collection, query) {
|
|
250
|
+
validateQuery(query);
|
|
251
|
+
return (await this.collection(collection).deleteMany(toMongoWhere(query.where))).deletedCount;
|
|
252
|
+
}
|
|
253
|
+
async exists(collection, query) {
|
|
254
|
+
validateQuery(query);
|
|
255
|
+
return await this.collection(collection).findOne(toMongoWhere(query.where), { projection: { _id: 1 } }) !== null;
|
|
256
|
+
}
|
|
257
|
+
async count(collection, query = {}) {
|
|
258
|
+
validateQuery(query);
|
|
259
|
+
return this.collection(collection).countDocuments(toMongoWhere(query.where));
|
|
260
|
+
}
|
|
261
|
+
async atomicBatch(operations) {
|
|
262
|
+
if (!this.capabilities.atomicBatch || !this.client) throw new StorageAtomicityError("MongoDB atomicBatch requires an explicitly enabled transaction-capable deployment.");
|
|
263
|
+
const session = this.client.startSession();
|
|
264
|
+
const results = [];
|
|
265
|
+
try {
|
|
266
|
+
await session.withTransaction(async () => {
|
|
267
|
+
results.length = 0;
|
|
268
|
+
for (const operation of operations) {
|
|
269
|
+
const target = this.collection(operation.collection);
|
|
270
|
+
if (operation.type === "insert") {
|
|
271
|
+
const document = toDocument(operation.record);
|
|
272
|
+
await target.insertOne(document, { session });
|
|
273
|
+
results.push({
|
|
274
|
+
type: "insert",
|
|
275
|
+
record: toRecord(document)
|
|
276
|
+
});
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (operation.type === "update") {
|
|
280
|
+
if ((await target.updateOne({
|
|
281
|
+
_id: operation.id,
|
|
282
|
+
...buildCondition(operation.condition)
|
|
283
|
+
}, {
|
|
284
|
+
$set: {
|
|
285
|
+
...cleanChanges(operation.changes),
|
|
286
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
287
|
+
},
|
|
288
|
+
$inc: { revision: 1 }
|
|
289
|
+
}, { session })).matchedCount === 0 && operation.condition) throw new StorageConflictError();
|
|
290
|
+
const updated = await target.findOne({ _id: operation.id }, { session });
|
|
291
|
+
results.push({
|
|
292
|
+
type: "update",
|
|
293
|
+
record: toRecord(updated)
|
|
294
|
+
});
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const result = await target.deleteOne({
|
|
298
|
+
_id: operation.id,
|
|
299
|
+
...buildCondition(operation.condition)
|
|
300
|
+
}, { session });
|
|
301
|
+
if (result.deletedCount === 0 && operation.condition && await target.findOne({ _id: operation.id }, { session })) throw new StorageConflictError();
|
|
302
|
+
results.push({
|
|
303
|
+
type: "delete",
|
|
304
|
+
deleted: result.deletedCount > 0
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
return { results };
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof StorageConflictError || error instanceof StorageAtomicityError) throw error;
|
|
311
|
+
if (isDuplicateError(error)) throw new StorageDuplicateError("mongodb_unique_constraint");
|
|
312
|
+
throw error;
|
|
313
|
+
} finally {
|
|
314
|
+
await session.endSession();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
export { MongoStorageAdapter };
|
|
319
|
+
|
|
320
|
+
//# sourceMappingURL=mongodb.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mongodb.adapter.js","names":[],"sources":["../src/mongodb.adapter.ts"],"sourcesContent":["import {\n type Document,\n type Filter,\n type MongoServerError,\n type Sort,\n type UpdateFilter,\n type WithId,\n} from \"mongodb\";\n\nimport {\n StorageAtomicityError,\n StorageConflictError,\n StorageDuplicateError,\n StorageValidationError,\n type StorageAdapter,\n type StorageBatchOperation,\n type StorageBatchResult,\n type StorageCollection,\n type StorageCollectionName,\n type StorageCondition,\n type StorageCursor,\n type StorageCursorQuery,\n type StoragePage,\n type StorageQuery,\n type StorageRecord,\n type StorageUpdate,\n type StorageWhere,\n type StorageCapabilities,\n type StorageCapabilityReport,\n validateQuery,\n type StorageInsert,\n} from \"@puzzle-ui/storage\";\n\nimport type { MongoStorageAdapterOptions } from \"./mongodb.types\";\n\ntype MongoRecord = Document & {\n _id: string;\n createdAt: Date;\n updatedAt: Date;\n revision: number;\n};\n\ntype CursorPayload = {\n readonly values: readonly unknown[];\n};\n\nconst isDuplicateError = (error: unknown): error is MongoServerError =>\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as MongoServerError).code === 11000;\n\nconst mapMongoError = (error: unknown): never => {\n if (isDuplicateError(error)) {\n throw new StorageDuplicateError(\"mongodb_unique_constraint\");\n }\n\n throw error;\n};\n\nconst toMongoField = (field: string) => (field === \"id\" ? \"_id\" : field);\n\nconst toMongoValue = (value: unknown) =>\n value instanceof Date ? value : value;\n\nconst encodeCursor = (payload: CursorPayload): StorageCursor => ({\n value: Buffer.from(JSON.stringify(payload)).toString(\"base64url\"),\n});\n\nconst decodeCursor = (cursor: StorageCursor): CursorPayload => {\n try {\n return JSON.parse(\n Buffer.from(cursor.value, \"base64url\").toString(\"utf8\"),\n ) as CursorPayload;\n } catch {\n throw new StorageValidationError(\"Invalid MongoDB cursor.\");\n }\n};\n\nconst toRecord = <T extends StorageRecord>(\n document: WithId<MongoRecord> | null,\n): T | null => {\n if (!document) return null;\n\n const { _id, ...data } = document;\n\n return {\n ...data,\n id: String(_id),\n } as T;\n};\n\nconst toDocument = <T extends StorageRecord>(\n record: Partial<T> & { id: string },\n): MongoRecord => {\n const { id, createdAt, updatedAt, revision, ...data } =\n record as StorageRecord & Document;\n\n const now = new Date();\n\n return {\n ...data,\n _id: id,\n createdAt: createdAt ?? now,\n updatedAt: updatedAt ?? now,\n revision: revision ?? 1,\n };\n};\n\nconst toMongoWhere = <T extends object>(\n where?: StorageWhere<T>,\n): Filter<MongoRecord> => {\n if (!where) return {};\n\n const result: Filter<MongoRecord>[] = [];\n\n for (const [field, comparison] of Object.entries(where)) {\n if (field === \"and\" || field === \"or\") {\n const nested = (comparison as readonly StorageWhere<T>[]).map((item) =>\n toMongoWhere(item),\n );\n\n result.push({\n [field === \"and\" ? \"$and\" : \"$or\"]: nested,\n });\n\n continue;\n }\n\n const value: unknown = comparison;\n\n if (\n typeof value !== \"object\" ||\n value === null ||\n !(\"operator\" in value) ||\n typeof value.operator !== \"string\" ||\n !(\"value\" in value)\n ) {\n throw new StorageValidationError(\n `Invalid query comparison for field: ${field}`,\n );\n }\n\n const mongoField = toMongoField(field);\n\n const operators: Record<string, string> = {\n eq: \"$eq\",\n ne: \"$ne\",\n in: \"$in\",\n notIn: \"$nin\",\n gt: \"$gt\",\n gte: \"$gte\",\n lt: \"$lt\",\n lte: \"$lte\",\n exists: \"$exists\",\n };\n\n const operator = operators[value.operator];\n\n if (!operator) {\n throw new StorageValidationError(\n `Unsupported query operator: ${value.operator}`,\n );\n }\n\n result.push({\n [mongoField]: {\n [operator]: value.value,\n },\n });\n }\n\n if (result.length === 0) return {};\n if (result.length === 1) return result[0]!;\n\n return { $and: result };\n};\n\nconst toMongoSort = <T extends StorageRecord>(\n orderBy?: StorageQuery<T>[\"orderBy\"],\n): Sort => {\n const sort: Record<string, 1 | -1> = {};\n\n for (const item of orderBy ?? []) {\n sort[toMongoField(String(item.field))] = item.direction === \"asc\" ? 1 : -1;\n }\n\n if (!(\"_id\" in sort)) {\n sort._id = 1;\n }\n\n return sort;\n};\n\nconst toCursorFilter = <T extends StorageRecord>(\n orderBy: NonNullable<StorageQuery<T>[\"orderBy\"]>,\n values: readonly unknown[],\n): Filter<MongoRecord> => {\n const fields = [\n ...orderBy.map((item) => ({\n field: toMongoField(String(item.field)),\n direction: item.direction,\n })),\n ];\n\n if (!fields.some((item) => item.field === \"_id\")) {\n fields.push({ field: \"_id\", direction: \"asc\" });\n }\n\n const branches: Filter<MongoRecord>[] = [];\n\n for (let index = 0; index < fields.length; index++) {\n const prefix: Filter<MongoRecord>[] = [];\n\n for (let previous = 0; previous < index; previous++) {\n prefix.push({\n [fields[previous]!.field]: values[previous],\n });\n }\n\n const field = fields[index]!;\n const operator = field.direction === \"asc\" ? \"$gt\" : \"$lt\";\n\n branches.push({\n $and: [\n ...prefix,\n {\n [field.field]: {\n [operator]: values[index],\n },\n },\n ],\n });\n }\n\n return { $or: branches };\n};\n\nconst buildCondition = (condition?: StorageCondition): Document => {\n if (!condition) return {};\n\n const result: Document = {};\n\n if (condition.expectedRevision !== undefined) {\n result.revision = condition.expectedRevision;\n }\n\n if (condition.expectedUpdatedAt !== undefined) {\n result.updatedAt = condition.expectedUpdatedAt;\n }\n\n return result;\n};\n\nconst cleanChanges = <T extends StorageRecord>(\n changes: StorageUpdate<T>,\n): Document => {\n const result = { ...changes } as Document;\n\n delete result.id;\n delete result.createdAt;\n delete result.updatedAt;\n delete result.revision;\n\n return result;\n};\n\nexport class MongoStorageAdapter implements StorageAdapter {\n readonly capabilities: StorageCapabilities;\n\n private readonly database: MongoStorageAdapterOptions[\"database\"];\n private readonly client: MongoStorageAdapterOptions[\"client\"];\n private readonly prefix: string;\n\n constructor(options: MongoStorageAdapterOptions) {\n this.database = options.database;\n this.client = options.client;\n this.prefix = options.collectionPrefix ?? \"puzzle_\";\n\n const transactionsEnabled =\n options.transactionsEnabled === true && !!options.client;\n\n this.capabilities = {\n transactions: transactionsEnabled,\n atomicBatch: transactionsEnabled,\n conditionalWrites: true,\n uniqueConstraints: true,\n upsert: true,\n cursorPagination: true,\n migrations: true,\n ttlIndexes: true,\n };\n }\n\n getCapabilities(): StorageCapabilityReport {\n const limitations = this.capabilities.transactions\n ? []\n : [\n \"Multi-document transactions are disabled. Configure a replica set or sharded deployment and set transactionsEnabled to true.\",\n ];\n\n return {\n capabilities: this.capabilities,\n limitations,\n };\n }\n\n private collection<T extends StorageRecord>(name: StorageCollectionName) {\n return this.database.collection<MongoRecord>(`${this.prefix}${name}`);\n }\n\n async registerCollection<T extends StorageRecord>(\n definition: StorageCollection<T>,\n ): Promise<void> {\n const collectionName = `${this.prefix}${definition.name}`;\n\n try {\n await this.database.createCollection(collectionName);\n } catch (error) {\n if (\n !(\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as { code: number }).code === 48\n )\n ) {\n throw error;\n }\n }\n\n const collection = this.database.collection(collectionName);\n\n const indexes = (definition.indexes ?? []).map((index) => ({\n key: Object.fromEntries(\n index.fields.map((field) => [toMongoField(String(field)), 1]),\n ),\n name: index.name,\n unique: index.unique ?? false,\n ...(index.expireAfterSeconds !== undefined\n ? { expireAfterSeconds: index.expireAfterSeconds }\n : {}),\n }));\n\n if (indexes.length > 0) {\n await collection.createIndexes(indexes);\n }\n }\n\n async findById<T extends StorageRecord>(\n collection: StorageCollectionName,\n id: string,\n ): Promise<T | null> {\n const document = await this.collection(collection).findOne({\n _id: id,\n });\n\n return toRecord<T>(document);\n }\n\n async findOne<T extends StorageRecord>(\n collection: StorageCollectionName,\n query: StorageQuery<T>,\n ): Promise<T | null> {\n validateQuery(query);\n\n const document = await this.collection(collection).findOne(\n toMongoWhere(query.where),\n {\n sort: toMongoSort(query.orderBy),\n ...(query.offset !== undefined ? { skip: query.offset } : {}),\n },\n );\n\n return toRecord<T>(document);\n }\n\n async findMany<T extends StorageRecord>(\n collection: StorageCollectionName,\n query: StorageQuery<T> = {},\n ): Promise<readonly T[]> {\n validateQuery(query);\n\n const documents = await this.collection(collection)\n .find(toMongoWhere(query.where))\n .sort(toMongoSort(query.orderBy))\n .skip(query.offset ?? 0)\n .limit(query.limit ?? 0)\n .toArray();\n\n return documents.map((document) => toRecord<T>(document) as T);\n }\n\n async findPage<T extends StorageRecord>(\n collection: StorageCollectionName,\n query: StorageCursorQuery<T>,\n ): Promise<StoragePage<T>> {\n if (!Number.isInteger(query.first) || query.first < 1) {\n throw new StorageValidationError(\n \"Cursor page size must be a positive integer.\",\n );\n }\n\n const orderBy = query.orderBy ?? [\n { field: \"id\" as keyof T & string, direction: \"asc\" as const },\n ];\n\n let filter = toMongoWhere(query.where);\n\n if (query.after) {\n const payload = decodeCursor(query.after);\n\n const cursorFilter = toCursorFilter(orderBy, payload.values);\n\n filter = Object.keys(filter).length\n ? { $and: [filter, cursorFilter] }\n : cursorFilter;\n }\n\n const sort = toMongoSort(orderBy);\n\n const documents = await this.collection(collection)\n .find(filter)\n .sort(sort)\n .limit(query.first + 1)\n .toArray();\n\n const hasNextPage = documents.length > query.first;\n const items = documents.slice(0, query.first);\n const last = items.at(-1);\n\n const cursorFields = [\n ...orderBy.map((item) => toMongoField(String(item.field))),\n ];\n\n if (!cursorFields.includes(\"_id\")) cursorFields.push(\"_id\");\n\n const nextCursor =\n hasNextPage && last\n ? encodeCursor({\n values: cursorFields.map((field) =>\n field === \"_id\" ? last._id : last[field],\n ),\n })\n : null;\n\n return {\n items: items.map((item) => toRecord<T>(item) as T),\n hasNextPage,\n nextCursor,\n };\n }\n\n async insert<T extends StorageRecord>(\n collection: StorageCollectionName,\n record: Parameters<StorageAdapter[\"insert\"]>[1],\n ): Promise<T> {\n const document = toDocument(record as StorageRecord & { id: string });\n\n try {\n await this.collection(collection).insertOne(document);\n return toRecord<T>(document as WithId<MongoRecord>) as T;\n } catch (error) {\n return mapMongoError(error);\n }\n }\n\n async upsert<T extends StorageRecord>(\n collection: StorageCollectionName,\n query: StorageQuery<T>,\n record: StorageInsert<T>,\n ): Promise<T> {\n validateQuery(query);\n\n const existing = await this.findOne<T>(collection, query);\n\n if (existing) {\n const updated = await this.update<T>(\n collection,\n existing.id,\n record as StorageUpdate<T>,\n );\n\n return updated ?? existing;\n }\n\n return this.insert<T>(collection, record);\n }\n\n async update<T extends StorageRecord>(\n collection: StorageCollectionName,\n id: string,\n changes: StorageUpdate<T>,\n condition?: StorageCondition,\n ): Promise<T | null> {\n const target = this.collection(collection);\n\n const filter = {\n _id: id,\n ...buildCondition(condition),\n };\n\n const update: UpdateFilter<MongoRecord> = {\n $set: {\n ...cleanChanges(changes),\n updatedAt: new Date(),\n },\n $inc: { revision: 1 },\n };\n\n const result = await target.updateOne(filter, update);\n\n if (result.matchedCount === 0) {\n if (condition && (await target.findOne({ _id: id }))) {\n throw new StorageConflictError();\n }\n\n return null;\n }\n\n return toRecord<T>(await target.findOne({ _id: id }));\n }\n\n async updateMany<T extends StorageRecord>(\n collection: StorageCollectionName,\n query: StorageQuery<T>,\n changes: StorageUpdate<T>,\n ): Promise<number> {\n validateQuery(query);\n\n const result = await this.collection(collection).updateMany(\n toMongoWhere(query.where),\n {\n $set: {\n ...cleanChanges(changes),\n updatedAt: new Date(),\n },\n $inc: { revision: 1 },\n },\n );\n\n return result.modifiedCount;\n }\n\n async delete<T extends StorageRecord>(\n collection: StorageCollectionName,\n id: string,\n condition?: StorageCondition,\n ): Promise<boolean> {\n const target = this.collection(collection);\n\n const result = await target.deleteOne({\n _id: id,\n ...buildCondition(condition),\n });\n\n if (result.deletedCount > 0) return true;\n\n if (condition && (await target.findOne({ _id: id }))) {\n throw new StorageConflictError();\n }\n\n return false;\n }\n\n async deleteMany<T extends StorageRecord>(\n collection: StorageCollectionName,\n query: StorageQuery<T>,\n ): Promise<number> {\n validateQuery(query);\n\n const result = await this.collection(collection).deleteMany(\n toMongoWhere(query.where),\n );\n\n return result.deletedCount;\n }\n\n async exists<T extends StorageRecord>(\n collection: StorageCollectionName,\n query: StorageQuery<T>,\n ): Promise<boolean> {\n validateQuery(query);\n\n return (\n (await this.collection(collection).findOne(toMongoWhere(query.where), {\n projection: { _id: 1 },\n })) !== null\n );\n }\n\n async count<T extends StorageRecord>(\n collection: StorageCollectionName,\n query: StorageQuery<T> = {},\n ): Promise<number> {\n validateQuery(query);\n\n return this.collection(collection).countDocuments(\n toMongoWhere(query.where),\n );\n }\n\n async atomicBatch<T extends StorageRecord>(\n operations: readonly StorageBatchOperation<T>[],\n ): Promise<StorageBatchResult<T>> {\n if (!this.capabilities.atomicBatch || !this.client) {\n throw new StorageAtomicityError(\n \"MongoDB atomicBatch requires an explicitly enabled transaction-capable deployment.\",\n );\n }\n\n const session = this.client.startSession();\n const results: StorageBatchResult<T>[\"results\"][number][] = [];\n\n try {\n await session.withTransaction(async () => {\n results.length = 0;\n\n for (const operation of operations) {\n const target = this.collection(operation.collection);\n\n if (operation.type === \"insert\") {\n const document = toDocument(\n operation.record as StorageRecord & { id: string },\n );\n\n await target.insertOne(document, { session });\n\n results.push({\n type: \"insert\",\n record: toRecord<T>(document as WithId<MongoRecord>) as T,\n });\n\n continue;\n }\n\n if (operation.type === \"update\") {\n const result = await target.updateOne(\n {\n _id: operation.id,\n ...buildCondition(operation.condition),\n },\n {\n $set: {\n ...cleanChanges(operation.changes),\n updatedAt: new Date(),\n },\n $inc: { revision: 1 },\n },\n { session },\n );\n\n if (result.matchedCount === 0 && operation.condition) {\n throw new StorageConflictError();\n }\n\n const updated = await target.findOne(\n { _id: operation.id },\n { session },\n );\n\n results.push({\n type: \"update\",\n record: toRecord<T>(updated),\n });\n\n continue;\n }\n\n const result = await target.deleteOne(\n {\n _id: operation.id,\n ...buildCondition(operation.condition),\n },\n { session },\n );\n\n if (\n result.deletedCount === 0 &&\n operation.condition &&\n (await target.findOne({ _id: operation.id }, { session }))\n ) {\n throw new StorageConflictError();\n }\n\n results.push({\n type: \"delete\",\n deleted: result.deletedCount > 0,\n });\n }\n });\n\n return { results };\n } catch (error) {\n if (\n error instanceof StorageConflictError ||\n error instanceof StorageAtomicityError\n ) {\n throw error;\n }\n\n if (isDuplicateError(error)) {\n throw new StorageDuplicateError(\"mongodb_unique_constraint\");\n }\n\n throw error;\n } finally {\n await session.endSession();\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
require("@puzzle-ui/storage");
|
|
2
|
+
let node_crypto = require("node:crypto");
|
|
3
|
+
// src/mongodb.migrations.ts
|
|
4
|
+
const sleep = (duration) => new Promise((resolve) => setTimeout(resolve, duration));
|
|
5
|
+
const isDuplicate = (error) => typeof error === "object" && error !== null && "code" in error && error.code === 11e3;
|
|
6
|
+
var MongoStorageMigrationAdapter = class {
|
|
7
|
+
database;
|
|
8
|
+
prefix;
|
|
9
|
+
lockDurationMs;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.database = options.database;
|
|
12
|
+
this.prefix = options.collectionPrefix ?? "puzzle_";
|
|
13
|
+
this.lockDurationMs = options.lockDurationMs ?? 6e5;
|
|
14
|
+
}
|
|
15
|
+
get migrations() {
|
|
16
|
+
return this.database.collection(`${this.prefix}migrations`);
|
|
17
|
+
}
|
|
18
|
+
get locks() {
|
|
19
|
+
return this.database.collection(`${this.prefix}migration_locks`);
|
|
20
|
+
}
|
|
21
|
+
async ensureCollections() {
|
|
22
|
+
for (const name of [`${this.prefix}migrations`, `${this.prefix}migration_locks`]) try {
|
|
23
|
+
await this.database.createCollection(name);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (!(typeof error === "object" && error !== null && "code" in error && error.code === 48)) throw error;
|
|
26
|
+
}
|
|
27
|
+
await this.locks.createIndexes([{
|
|
28
|
+
key: { expiresAt: 1 },
|
|
29
|
+
name: "expiresAt_ttl",
|
|
30
|
+
expireAfterSeconds: 0
|
|
31
|
+
}]);
|
|
32
|
+
}
|
|
33
|
+
async getMigrationStatus() {
|
|
34
|
+
await this.ensureCollections();
|
|
35
|
+
const migrations = (await this.migrations.find({ _id: { $ne: "__lock__" } }).sort({ version: 1 }).toArray()).map((record) => ({
|
|
36
|
+
id: record._id,
|
|
37
|
+
version: record.version,
|
|
38
|
+
description: record.description,
|
|
39
|
+
status: record.status,
|
|
40
|
+
...record.startedAt ? { startedAt: record.startedAt } : {},
|
|
41
|
+
...record.completedAt ? { completedAt: record.completedAt } : {},
|
|
42
|
+
...record.error ? { error: record.error } : {}
|
|
43
|
+
}));
|
|
44
|
+
return {
|
|
45
|
+
currentVersion: migrations.reduce((version, migration) => migration.status === "completed" ? Math.max(version, migration.version) : version, 0),
|
|
46
|
+
migrations
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async acquireLock(owner) {
|
|
50
|
+
const deadline = Date.now() + this.lockDurationMs;
|
|
51
|
+
while (Date.now() < deadline) {
|
|
52
|
+
const now = /* @__PURE__ */ new Date();
|
|
53
|
+
const expiresAt = new Date(now.getTime() + this.lockDurationMs);
|
|
54
|
+
try {
|
|
55
|
+
await this.locks.updateOne({
|
|
56
|
+
_id: "global",
|
|
57
|
+
$or: [{ expiresAt: { $lt: now } }, { owner }]
|
|
58
|
+
}, { $set: {
|
|
59
|
+
owner,
|
|
60
|
+
expiresAt
|
|
61
|
+
} }, { upsert: true });
|
|
62
|
+
if ((await this.locks.findOne({ _id: "global" }))?.owner === owner) return;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (!isDuplicate(error)) throw error;
|
|
65
|
+
}
|
|
66
|
+
await sleep(50);
|
|
67
|
+
}
|
|
68
|
+
throw new Error("Timed out while acquiring the migration lock.");
|
|
69
|
+
}
|
|
70
|
+
async execute(operation) {
|
|
71
|
+
switch (operation.type) {
|
|
72
|
+
case "createCollection":
|
|
73
|
+
try {
|
|
74
|
+
await this.database.createCollection(`${this.prefix}${operation.collection}`);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (!(typeof error === "object" && error !== null && "code" in error && error.code === 48)) throw error;
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
case "dropCollection":
|
|
80
|
+
await this.database.dropCollection(`${this.prefix}${operation.collection}`).catch(() => false);
|
|
81
|
+
return;
|
|
82
|
+
case "createIndex":
|
|
83
|
+
await this.database.collection(`${this.prefix}${operation.collection}`).createIndexes([{
|
|
84
|
+
key: Object.fromEntries(operation.fields.map((field) => [field, 1])),
|
|
85
|
+
name: operation.name,
|
|
86
|
+
unique: operation.unique ?? false,
|
|
87
|
+
...operation.expireAfterSeconds !== void 0 ? { expireAfterSeconds: operation.expireAfterSeconds } : {}
|
|
88
|
+
}]);
|
|
89
|
+
return;
|
|
90
|
+
case "dropIndex":
|
|
91
|
+
await this.database.collection(`${this.prefix}${operation.collection}`).dropIndex(operation.name);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async runMigrations(migrations, options = {}) {
|
|
96
|
+
await this.ensureCollections();
|
|
97
|
+
const owner = (0, node_crypto.randomUUID)();
|
|
98
|
+
await this.acquireLock(owner);
|
|
99
|
+
const applied = [];
|
|
100
|
+
const skipped = [];
|
|
101
|
+
try {
|
|
102
|
+
const sorted = [...migrations].sort((a, b) => a.version - b.version);
|
|
103
|
+
for (const migration of sorted) {
|
|
104
|
+
if (options.targetVersion !== void 0 && migration.version > options.targetVersion) {
|
|
105
|
+
skipped.push(migration.version);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if ((await this.migrations.findOne({ _id: migration.id }))?.status === "completed") {
|
|
109
|
+
skipped.push(migration.version);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (options.dryRun) {
|
|
113
|
+
applied.push(migration.version);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
117
|
+
await this.migrations.updateOne({ _id: migration.id }, {
|
|
118
|
+
$set: {
|
|
119
|
+
version: migration.version,
|
|
120
|
+
description: migration.description,
|
|
121
|
+
status: "running",
|
|
122
|
+
startedAt
|
|
123
|
+
},
|
|
124
|
+
$unset: {
|
|
125
|
+
completedAt: "",
|
|
126
|
+
error: ""
|
|
127
|
+
}
|
|
128
|
+
}, { upsert: true });
|
|
129
|
+
try {
|
|
130
|
+
await migration.up({ execute: (operation) => this.execute(operation) });
|
|
131
|
+
await this.migrations.updateOne({ _id: migration.id }, { $set: {
|
|
132
|
+
status: "completed",
|
|
133
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
134
|
+
} });
|
|
135
|
+
applied.push(migration.version);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
await this.migrations.updateOne({ _id: migration.id }, { $set: {
|
|
138
|
+
status: "failed",
|
|
139
|
+
error: error instanceof Error ? error.message : String(error)
|
|
140
|
+
} });
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} finally {
|
|
145
|
+
await this.locks.deleteOne({
|
|
146
|
+
_id: "global",
|
|
147
|
+
owner
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
applied,
|
|
152
|
+
skipped,
|
|
153
|
+
currentVersion: (await this.getMigrationStatus()).currentVersion
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
exports.MongoStorageMigrationAdapter = MongoStorageMigrationAdapter;
|
|
158
|
+
|
|
159
|
+
//# sourceMappingURL=mongodb.migrations.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mongodb.migrations.cjs","names":["randomUUID"],"sources":["../src/mongodb.migrations.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Db, Document } from \"mongodb\";\n\nimport {\n type StorageMigration,\n type StorageMigrationAdapter,\n type StorageMigrationOperation,\n type StorageMigrationOptions,\n type StorageMigrationRecord,\n type StorageMigrationResult,\n} from \"@puzzle-ui/storage\";\n\nimport type { MongoStorageMigrationOptions } from \"./mongodb.types\";\n\ntype MigrationDocument = Document & {\n _id: string;\n version: number;\n description: string;\n status: \"running\" | \"completed\" | \"failed\";\n startedAt?: Date;\n completedAt?: Date;\n error?: string;\n};\n\ntype MigrationLockDocument = Document & {\n _id: string;\n owner: string;\n expiresAt: Date;\n};\n\nconst sleep = (duration: number) =>\n new Promise((resolve) => setTimeout(resolve, duration));\n\nconst isDuplicate = (error: unknown) =>\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as { code: number }).code === 11000;\n\nexport class MongoStorageMigrationAdapter implements StorageMigrationAdapter {\n private readonly database: Db;\n private readonly prefix: string;\n private readonly lockDurationMs: number;\n\n constructor(options: MongoStorageMigrationOptions) {\n this.database = options.database;\n this.prefix = options.collectionPrefix ?? \"puzzle_\";\n this.lockDurationMs = options.lockDurationMs ?? 600_000;\n }\n\n private get migrations() {\n return this.database.collection<MigrationDocument>(\n `${this.prefix}migrations`,\n );\n }\n\n private get locks() {\n return this.database.collection<MigrationLockDocument>(\n `${this.prefix}migration_locks`,\n );\n }\n\n private async ensureCollections() {\n for (const name of [\n `${this.prefix}migrations`,\n `${this.prefix}migration_locks`,\n ]) {\n try {\n await this.database.createCollection(name);\n } catch (error) {\n if (\n !(\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as { code: number }).code === 48\n )\n ) {\n throw error;\n }\n }\n }\n\n await this.locks.createIndexes([\n {\n key: { expiresAt: 1 },\n name: \"expiresAt_ttl\",\n expireAfterSeconds: 0,\n },\n ]);\n }\n\n async getMigrationStatus(): Promise<{\n currentVersion: number;\n migrations: readonly StorageMigrationRecord[];\n }> {\n await this.ensureCollections();\n\n const records = await this.migrations\n .find({ _id: { $ne: \"__lock__\" } })\n .sort({ version: 1 })\n .toArray();\n\n const migrations = records.map((record) => ({\n id: record._id,\n version: record.version,\n description: record.description,\n status: record.status,\n ...(record.startedAt ? { startedAt: record.startedAt } : {}),\n ...(record.completedAt ? { completedAt: record.completedAt } : {}),\n ...(record.error ? { error: record.error } : {}),\n })) satisfies StorageMigrationRecord[];\n\n return {\n currentVersion: migrations.reduce(\n (version, migration) =>\n migration.status === \"completed\"\n ? Math.max(version, migration.version)\n : version,\n 0,\n ),\n migrations,\n };\n }\n\n private async acquireLock(owner: string): Promise<void> {\n const deadline = Date.now() + this.lockDurationMs;\n\n while (Date.now() < deadline) {\n const now = new Date();\n const expiresAt = new Date(now.getTime() + this.lockDurationMs);\n\n try {\n await this.locks.updateOne(\n {\n _id: \"global\",\n $or: [{ expiresAt: { $lt: now } }, { owner }],\n },\n {\n $set: { owner, expiresAt },\n },\n { upsert: true },\n );\n\n const lock = await this.locks.findOne({ _id: \"global\" });\n\n if (lock?.owner === owner) return;\n } catch (error) {\n if (!isDuplicate(error)) throw error;\n }\n\n await sleep(50);\n }\n\n throw new Error(\"Timed out while acquiring the migration lock.\");\n }\n\n private async execute(operation: StorageMigrationOperation): Promise<void> {\n switch (operation.type) {\n case \"createCollection\": {\n try {\n await this.database.createCollection(\n `${this.prefix}${operation.collection}`,\n );\n } catch (error) {\n if (\n !(\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as { code: number }).code === 48\n )\n ) {\n throw error;\n }\n }\n\n return;\n }\n\n case \"dropCollection\":\n await this.database\n .dropCollection(`${this.prefix}${operation.collection}`)\n .catch(() => false);\n return;\n\n case \"createIndex\": {\n const collection = this.database.collection(\n `${this.prefix}${operation.collection}`,\n );\n\n await collection.createIndexes([\n {\n key: Object.fromEntries(\n operation.fields.map((field) => [field, 1]),\n ),\n name: operation.name,\n unique: operation.unique ?? false,\n ...(operation.expireAfterSeconds !== undefined\n ? { expireAfterSeconds: operation.expireAfterSeconds }\n : {}),\n },\n ]);\n\n return;\n }\n\n case \"dropIndex\":\n await this.database\n .collection(`${this.prefix}${operation.collection}`)\n .dropIndex(operation.name);\n return;\n }\n }\n\n async runMigrations(\n migrations: readonly StorageMigration[],\n options: StorageMigrationOptions = {},\n ): Promise<StorageMigrationResult> {\n await this.ensureCollections();\n\n const owner = randomUUID();\n await this.acquireLock(owner);\n\n const applied: number[] = [];\n const skipped: number[] = [];\n\n try {\n const sorted = [...migrations].sort((a, b) => a.version - b.version);\n\n for (const migration of sorted) {\n if (\n options.targetVersion !== undefined &&\n migration.version > options.targetVersion\n ) {\n skipped.push(migration.version);\n continue;\n }\n\n const existing = await this.migrations.findOne({\n _id: migration.id,\n });\n\n if (existing?.status === \"completed\") {\n skipped.push(migration.version);\n continue;\n }\n\n if (options.dryRun) {\n applied.push(migration.version);\n continue;\n }\n\n const startedAt = new Date();\n\n await this.migrations.updateOne(\n { _id: migration.id },\n {\n $set: {\n version: migration.version,\n description: migration.description,\n status: \"running\",\n startedAt,\n },\n $unset: {\n completedAt: \"\",\n error: \"\",\n },\n },\n { upsert: true },\n );\n\n try {\n await migration.up({\n execute: (operation) => this.execute(operation),\n });\n\n await this.migrations.updateOne(\n { _id: migration.id },\n {\n $set: {\n status: \"completed\",\n completedAt: new Date(),\n },\n },\n );\n\n applied.push(migration.version);\n } catch (error) {\n await this.migrations.updateOne(\n { _id: migration.id },\n {\n $set: {\n status: \"failed\",\n error: error instanceof Error ? error.message : String(error),\n },\n },\n );\n\n throw error;\n }\n }\n } finally {\n await this.locks.deleteOne({\n _id: \"global\",\n owner,\n });\n }\n\n const status = await this.getMigrationStatus();\n\n return {\n applied,\n skipped,\n currentVersion: status.currentVersion,\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MongoStorageMigrationOptions } from "./mongodb.types.cjs";
|
|
2
|
+
import { StorageMigration, StorageMigrationAdapter, StorageMigrationOptions, StorageMigrationRecord, StorageMigrationResult } from "@puzzle-ui/storage";
|
|
3
|
+
export declare class MongoStorageMigrationAdapter implements StorageMigrationAdapter {
|
|
4
|
+
private readonly database;
|
|
5
|
+
private readonly prefix;
|
|
6
|
+
private readonly lockDurationMs;
|
|
7
|
+
constructor(options: MongoStorageMigrationOptions);
|
|
8
|
+
private get migrations();
|
|
9
|
+
private get locks();
|
|
10
|
+
private ensureCollections;
|
|
11
|
+
getMigrationStatus(): Promise<{
|
|
12
|
+
currentVersion: number;
|
|
13
|
+
migrations: readonly StorageMigrationRecord[];
|
|
14
|
+
}>;
|
|
15
|
+
private acquireLock;
|
|
16
|
+
private execute;
|
|
17
|
+
runMigrations(migrations: readonly StorageMigration[], options?: StorageMigrationOptions): Promise<StorageMigrationResult>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MongoStorageMigrationOptions } from "./mongodb.types.js";
|
|
2
|
+
import { StorageMigration, StorageMigrationAdapter, StorageMigrationOptions, StorageMigrationRecord, StorageMigrationResult } from "@puzzle-ui/storage";
|
|
3
|
+
export declare class MongoStorageMigrationAdapter implements StorageMigrationAdapter {
|
|
4
|
+
private readonly database;
|
|
5
|
+
private readonly prefix;
|
|
6
|
+
private readonly lockDurationMs;
|
|
7
|
+
constructor(options: MongoStorageMigrationOptions);
|
|
8
|
+
private get migrations();
|
|
9
|
+
private get locks();
|
|
10
|
+
private ensureCollections;
|
|
11
|
+
getMigrationStatus(): Promise<{
|
|
12
|
+
currentVersion: number;
|
|
13
|
+
migrations: readonly StorageMigrationRecord[];
|
|
14
|
+
}>;
|
|
15
|
+
private acquireLock;
|
|
16
|
+
private execute;
|
|
17
|
+
runMigrations(migrations: readonly StorageMigration[], options?: StorageMigrationOptions): Promise<StorageMigrationResult>;
|
|
18
|
+
}
|