@prisma-next/adapter-mongo 0.3.0-dev.146 → 0.3.0-dev.162

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.
@@ -0,0 +1,89 @@
1
+ import type {
2
+ CollModCommand,
3
+ CreateCollectionCommand,
4
+ CreateIndexCommand,
5
+ DropCollectionCommand,
6
+ DropIndexCommand,
7
+ ListCollectionsCommand,
8
+ ListIndexesCommand,
9
+ MongoDdlCommandVisitor,
10
+ MongoInspectionCommandVisitor,
11
+ } from '@prisma-next/mongo-query-ast/control';
12
+ import { keysToKeySpec } from '@prisma-next/mongo-query-ast/control';
13
+ import { type Db, type Document, MongoServerError } from 'mongodb';
14
+
15
+ export class MongoCommandExecutor implements MongoDdlCommandVisitor<Promise<void>> {
16
+ constructor(private readonly db: Db) {}
17
+
18
+ async createIndex(cmd: CreateIndexCommand): Promise<void> {
19
+ const keySpec: Document = keysToKeySpec(cmd.keys);
20
+ const options: Record<string, unknown> = {};
21
+ if (cmd.unique !== undefined) options['unique'] = cmd.unique;
22
+ if (cmd.sparse !== undefined) options['sparse'] = cmd.sparse;
23
+ if (cmd.expireAfterSeconds !== undefined)
24
+ options['expireAfterSeconds'] = cmd.expireAfterSeconds;
25
+ if (cmd.partialFilterExpression !== undefined)
26
+ options['partialFilterExpression'] = cmd.partialFilterExpression;
27
+ if (cmd.name !== undefined) options['name'] = cmd.name;
28
+ if (cmd.wildcardProjection !== undefined)
29
+ options['wildcardProjection'] = cmd.wildcardProjection;
30
+ if (cmd.collation !== undefined) options['collation'] = cmd.collation;
31
+ if (cmd.weights !== undefined) options['weights'] = cmd.weights;
32
+ if (cmd.default_language !== undefined) options['default_language'] = cmd.default_language;
33
+ if (cmd.language_override !== undefined) options['language_override'] = cmd.language_override;
34
+ await this.db.collection(cmd.collection).createIndex(keySpec, options);
35
+ }
36
+
37
+ async dropIndex(cmd: DropIndexCommand): Promise<void> {
38
+ await this.db.collection(cmd.collection).dropIndex(cmd.name);
39
+ }
40
+
41
+ async createCollection(cmd: CreateCollectionCommand): Promise<void> {
42
+ const options: Record<string, unknown> = {};
43
+ if (cmd.capped !== undefined) options['capped'] = cmd.capped;
44
+ if (cmd.size !== undefined) options['size'] = cmd.size;
45
+ if (cmd.max !== undefined) options['max'] = cmd.max;
46
+ if (cmd.timeseries !== undefined) options['timeseries'] = cmd.timeseries;
47
+ if (cmd.collation !== undefined) options['collation'] = cmd.collation;
48
+ if (cmd.clusteredIndex !== undefined) options['clusteredIndex'] = cmd.clusteredIndex;
49
+ if (cmd.validator !== undefined) options['validator'] = cmd.validator;
50
+ if (cmd.validationLevel !== undefined) options['validationLevel'] = cmd.validationLevel;
51
+ if (cmd.validationAction !== undefined) options['validationAction'] = cmd.validationAction;
52
+ if (cmd.changeStreamPreAndPostImages !== undefined)
53
+ options['changeStreamPreAndPostImages'] = cmd.changeStreamPreAndPostImages;
54
+ await this.db.createCollection(cmd.collection, options);
55
+ }
56
+
57
+ async dropCollection(cmd: DropCollectionCommand): Promise<void> {
58
+ await this.db.collection(cmd.collection).drop();
59
+ }
60
+
61
+ async collMod(cmd: CollModCommand): Promise<void> {
62
+ const command: Record<string, unknown> = { collMod: cmd.collection };
63
+ if (cmd.validator !== undefined) command['validator'] = cmd.validator;
64
+ if (cmd.validationLevel !== undefined) command['validationLevel'] = cmd.validationLevel;
65
+ if (cmd.validationAction !== undefined) command['validationAction'] = cmd.validationAction;
66
+ if (cmd.changeStreamPreAndPostImages !== undefined)
67
+ command['changeStreamPreAndPostImages'] = cmd.changeStreamPreAndPostImages;
68
+ await this.db.command(command);
69
+ }
70
+ }
71
+
72
+ export class MongoInspectionExecutor implements MongoInspectionCommandVisitor<Promise<Document[]>> {
73
+ constructor(private readonly db: Db) {}
74
+
75
+ async listIndexes(cmd: ListIndexesCommand): Promise<Document[]> {
76
+ try {
77
+ return await this.db.collection(cmd.collection).listIndexes().toArray();
78
+ } catch (error: unknown) {
79
+ if (error instanceof MongoServerError && error.code === 26) {
80
+ return [];
81
+ }
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ async listCollections(_cmd: ListCollectionsCommand): Promise<Document[]> {
87
+ return this.db.listCollections().toArray();
88
+ }
89
+ }
@@ -0,0 +1,63 @@
1
+ import type {
2
+ MongoContract,
3
+ MongoStorageCollection,
4
+ MongoStorageCollectionOptions,
5
+ MongoStorageIndex,
6
+ MongoStorageValidator,
7
+ } from '@prisma-next/mongo-contract';
8
+ import {
9
+ MongoSchemaCollection,
10
+ MongoSchemaCollectionOptions,
11
+ MongoSchemaIndex,
12
+ MongoSchemaIR,
13
+ MongoSchemaValidator,
14
+ } from '@prisma-next/mongo-schema-ir';
15
+
16
+ function convertIndex(index: MongoStorageIndex): MongoSchemaIndex {
17
+ return new MongoSchemaIndex({
18
+ keys: index.keys,
19
+ unique: index.unique,
20
+ sparse: index.sparse,
21
+ expireAfterSeconds: index.expireAfterSeconds,
22
+ partialFilterExpression: index.partialFilterExpression,
23
+ wildcardProjection: index.wildcardProjection,
24
+ collation: index.collation,
25
+ weights: index.weights,
26
+ default_language: index.default_language,
27
+ language_override: index.language_override,
28
+ });
29
+ }
30
+
31
+ function convertValidator(v: MongoStorageValidator): MongoSchemaValidator {
32
+ return new MongoSchemaValidator({
33
+ jsonSchema: v.jsonSchema,
34
+ validationLevel: v.validationLevel,
35
+ validationAction: v.validationAction,
36
+ });
37
+ }
38
+
39
+ function convertOptions(o: MongoStorageCollectionOptions): MongoSchemaCollectionOptions {
40
+ return new MongoSchemaCollectionOptions(o);
41
+ }
42
+
43
+ function convertCollection(name: string, def: MongoStorageCollection): MongoSchemaCollection {
44
+ const indexes = (def.indexes ?? []).map(convertIndex);
45
+ return new MongoSchemaCollection({
46
+ name,
47
+ indexes,
48
+ ...(def.validator != null && { validator: convertValidator(def.validator) }),
49
+ ...(def.options != null && { options: convertOptions(def.options) }),
50
+ });
51
+ }
52
+
53
+ export function contractToMongoSchemaIR(contract: MongoContract | null): MongoSchemaIR {
54
+ if (!contract) {
55
+ return new MongoSchemaIR([]);
56
+ }
57
+
58
+ const collections = Object.entries(contract.storage.collections).map(([name, def]) =>
59
+ convertCollection(name, def),
60
+ );
61
+
62
+ return new MongoSchemaIR(collections);
63
+ }
@@ -0,0 +1,112 @@
1
+ import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
2
+ import type {
3
+ CollModCommand,
4
+ CreateCollectionCommand,
5
+ CreateIndexCommand,
6
+ DropCollectionCommand,
7
+ DropIndexCommand,
8
+ MongoDdlCommandVisitor,
9
+ MongoIndexKey,
10
+ } from '@prisma-next/mongo-query-ast/control';
11
+
12
+ function formatKeySpec(keys: ReadonlyArray<MongoIndexKey>): string {
13
+ const entries = keys.map((k) => `${JSON.stringify(k.field)}: ${JSON.stringify(k.direction)}`);
14
+ return `{ ${entries.join(', ')} }`;
15
+ }
16
+
17
+ function formatOptions(cmd: CreateIndexCommand): string | undefined {
18
+ const parts: string[] = [];
19
+ if (cmd.unique) parts.push('unique: true');
20
+ if (cmd.sparse) parts.push('sparse: true');
21
+ if (cmd.expireAfterSeconds !== undefined)
22
+ parts.push(`expireAfterSeconds: ${cmd.expireAfterSeconds}`);
23
+ if (cmd.name) parts.push(`name: ${JSON.stringify(cmd.name)}`);
24
+ if (cmd.collation) parts.push(`collation: ${JSON.stringify(cmd.collation)}`);
25
+ if (cmd.weights) parts.push(`weights: ${JSON.stringify(cmd.weights)}`);
26
+ if (cmd.default_language) parts.push(`default_language: ${JSON.stringify(cmd.default_language)}`);
27
+ if (cmd.language_override)
28
+ parts.push(`language_override: ${JSON.stringify(cmd.language_override)}`);
29
+ if (cmd.wildcardProjection)
30
+ parts.push(`wildcardProjection: ${JSON.stringify(cmd.wildcardProjection)}`);
31
+ if (cmd.partialFilterExpression)
32
+ parts.push(`partialFilterExpression: ${JSON.stringify(cmd.partialFilterExpression)}`);
33
+ if (parts.length === 0) return undefined;
34
+ return `{ ${parts.join(', ')} }`;
35
+ }
36
+
37
+ function formatCreateCollectionOptions(cmd: CreateCollectionCommand): string | undefined {
38
+ const parts: string[] = [];
39
+ if (cmd.capped) parts.push('capped: true');
40
+ if (cmd.size !== undefined) parts.push(`size: ${cmd.size}`);
41
+ if (cmd.max !== undefined) parts.push(`max: ${cmd.max}`);
42
+ if (cmd.timeseries) parts.push(`timeseries: ${JSON.stringify(cmd.timeseries)}`);
43
+ if (cmd.collation) parts.push(`collation: ${JSON.stringify(cmd.collation)}`);
44
+ if (cmd.clusteredIndex) parts.push(`clusteredIndex: ${JSON.stringify(cmd.clusteredIndex)}`);
45
+ if (cmd.validator) parts.push(`validator: ${JSON.stringify(cmd.validator)}`);
46
+ if (cmd.validationLevel) parts.push(`validationLevel: ${JSON.stringify(cmd.validationLevel)}`);
47
+ if (cmd.validationAction) parts.push(`validationAction: ${JSON.stringify(cmd.validationAction)}`);
48
+ if (cmd.changeStreamPreAndPostImages)
49
+ parts.push(`changeStreamPreAndPostImages: ${JSON.stringify(cmd.changeStreamPreAndPostImages)}`);
50
+ if (parts.length === 0) return undefined;
51
+ return `{ ${parts.join(', ')} }`;
52
+ }
53
+
54
+ class MongoDdlCommandFormatter implements MongoDdlCommandVisitor<string> {
55
+ createIndex(cmd: CreateIndexCommand): string {
56
+ const keySpec = formatKeySpec(cmd.keys);
57
+ const opts = formatOptions(cmd);
58
+ return opts
59
+ ? `db.${cmd.collection}.createIndex(${keySpec}, ${opts})`
60
+ : `db.${cmd.collection}.createIndex(${keySpec})`;
61
+ }
62
+
63
+ dropIndex(cmd: DropIndexCommand): string {
64
+ return `db.${cmd.collection}.dropIndex(${JSON.stringify(cmd.name)})`;
65
+ }
66
+
67
+ createCollection(cmd: CreateCollectionCommand): string {
68
+ const opts = formatCreateCollectionOptions(cmd);
69
+ return opts
70
+ ? `db.createCollection(${JSON.stringify(cmd.collection)}, ${opts})`
71
+ : `db.createCollection(${JSON.stringify(cmd.collection)})`;
72
+ }
73
+
74
+ dropCollection(cmd: DropCollectionCommand): string {
75
+ return `db.${cmd.collection}.drop()`;
76
+ }
77
+
78
+ collMod(cmd: CollModCommand): string {
79
+ const parts: string[] = [`collMod: ${JSON.stringify(cmd.collection)}`];
80
+ if (cmd.validator) parts.push(`validator: ${JSON.stringify(cmd.validator)}`);
81
+ if (cmd.validationLevel) parts.push(`validationLevel: ${JSON.stringify(cmd.validationLevel)}`);
82
+ if (cmd.validationAction)
83
+ parts.push(`validationAction: ${JSON.stringify(cmd.validationAction)}`);
84
+ if (cmd.changeStreamPreAndPostImages)
85
+ parts.push(
86
+ `changeStreamPreAndPostImages: ${JSON.stringify(cmd.changeStreamPreAndPostImages)}`,
87
+ );
88
+ return `db.runCommand({ ${parts.join(', ')} })`;
89
+ }
90
+ }
91
+
92
+ const formatter = new MongoDdlCommandFormatter();
93
+
94
+ interface MongoExecuteStep {
95
+ readonly command: { readonly accept: <R>(visitor: MongoDdlCommandVisitor<R>) => R };
96
+ }
97
+
98
+ export function formatMongoOperations(operations: readonly MigrationPlanOperation[]): string[] {
99
+ const statements: string[] = [];
100
+ for (const operation of operations) {
101
+ const candidate = operation as unknown as Record<string, unknown>;
102
+ if (!('execute' in candidate) || !Array.isArray(candidate['execute'])) {
103
+ continue;
104
+ }
105
+ for (const step of candidate['execute'] as MongoExecuteStep[]) {
106
+ if (step.command && typeof step.command.accept === 'function') {
107
+ statements.push(step.command.accept(formatter));
108
+ }
109
+ }
110
+ }
111
+ return statements;
112
+ }
@@ -0,0 +1,84 @@
1
+ import type {
2
+ MongoAndExpr,
3
+ MongoExistsExpr,
4
+ MongoExprFilter,
5
+ MongoFieldFilter,
6
+ MongoFilterExpr,
7
+ MongoFilterVisitor,
8
+ MongoNotExpr,
9
+ MongoOrExpr,
10
+ } from '@prisma-next/mongo-query-ast/control';
11
+ import { deepEqual } from '@prisma-next/mongo-schema-ir';
12
+ import type { MongoValue } from '@prisma-next/mongo-value';
13
+
14
+ function getNestedField(doc: Record<string, unknown>, path: string): unknown {
15
+ const parts = path.split('.');
16
+ let current: unknown = doc;
17
+ for (const part of parts) {
18
+ if (current === null || current === undefined || typeof current !== 'object') {
19
+ return undefined;
20
+ }
21
+ const record = current as Record<string, unknown>;
22
+ if (!Object.hasOwn(record, part)) {
23
+ return undefined;
24
+ }
25
+ current = record[part];
26
+ }
27
+ return current;
28
+ }
29
+
30
+ function evaluateFieldOp(op: string, actual: unknown, expected: MongoValue): boolean {
31
+ switch (op) {
32
+ case '$eq':
33
+ return deepEqual(actual, expected);
34
+ case '$ne':
35
+ return !deepEqual(actual, expected);
36
+ case '$gt':
37
+ return typeof actual === typeof expected && (actual as number) > (expected as number);
38
+ case '$gte':
39
+ return typeof actual === typeof expected && (actual as number) >= (expected as number);
40
+ case '$lt':
41
+ return typeof actual === typeof expected && (actual as number) < (expected as number);
42
+ case '$lte':
43
+ return typeof actual === typeof expected && (actual as number) <= (expected as number);
44
+ case '$in':
45
+ return Array.isArray(expected) && expected.some((v) => deepEqual(actual, v));
46
+ default:
47
+ throw new Error(`Unsupported filter operator in migration check: ${op}`);
48
+ }
49
+ }
50
+
51
+ export class FilterEvaluator implements MongoFilterVisitor<boolean> {
52
+ private doc: Record<string, unknown> = {};
53
+
54
+ evaluate(filter: MongoFilterExpr, doc: Record<string, unknown>): boolean {
55
+ this.doc = doc;
56
+ return filter.accept(this);
57
+ }
58
+
59
+ field(expr: MongoFieldFilter): boolean {
60
+ const value = getNestedField(this.doc, expr.field);
61
+ return evaluateFieldOp(expr.op, value, expr.value);
62
+ }
63
+
64
+ and(expr: MongoAndExpr): boolean {
65
+ return expr.exprs.every((child) => child.accept(this));
66
+ }
67
+
68
+ or(expr: MongoOrExpr): boolean {
69
+ return expr.exprs.some((child) => child.accept(this));
70
+ }
71
+
72
+ not(expr: MongoNotExpr): boolean {
73
+ return !expr.expr.accept(this);
74
+ }
75
+
76
+ exists(expr: MongoExistsExpr): boolean {
77
+ const has = getNestedField(this.doc, expr.field) !== undefined;
78
+ return expr.exists ? has : !has;
79
+ }
80
+
81
+ expr(_expr: MongoExprFilter): boolean {
82
+ throw new Error('Aggregation expression filters are not supported in migration checks');
83
+ }
84
+ }
@@ -0,0 +1,118 @@
1
+ import type { MongoIndexKey, MongoIndexKeyDirection } from '@prisma-next/mongo-contract';
2
+ import {
3
+ MongoSchemaCollection,
4
+ MongoSchemaCollectionOptions,
5
+ MongoSchemaIndex,
6
+ MongoSchemaIR,
7
+ MongoSchemaValidator,
8
+ } from '@prisma-next/mongo-schema-ir';
9
+ import type { Db, Document } from 'mongodb';
10
+
11
+ const PRISMA_MIGRATIONS_COLLECTION = '_prisma_migrations';
12
+
13
+ function parseIndexKeys(keySpec: Record<string, unknown>): MongoIndexKey[] {
14
+ const keys: MongoIndexKey[] = [];
15
+ for (const [field, direction] of Object.entries(keySpec)) {
16
+ keys.push({ field, direction: direction as MongoIndexKeyDirection });
17
+ }
18
+ return keys;
19
+ }
20
+
21
+ function isDefaultIdIndex(doc: Document): boolean {
22
+ const key = doc['key'] as Record<string, unknown> | undefined;
23
+ if (!key) return false;
24
+ const entries = Object.entries(key);
25
+ return entries.length === 1 && entries[0]?.[0] === '_id' && entries[0]?.[1] === 1;
26
+ }
27
+
28
+ function parseIndex(doc: Document): MongoSchemaIndex {
29
+ const keySpec = doc['key'] as Record<string, unknown>;
30
+ return new MongoSchemaIndex({
31
+ keys: parseIndexKeys(keySpec),
32
+ unique: doc['unique'] as boolean | undefined,
33
+ sparse: doc['sparse'] as boolean | undefined,
34
+ expireAfterSeconds: doc['expireAfterSeconds'] as number | undefined,
35
+ partialFilterExpression: doc['partialFilterExpression'] as Record<string, unknown> | undefined,
36
+ wildcardProjection: doc['wildcardProjection'] as Record<string, 0 | 1> | undefined,
37
+ collation: doc['collation'] as Record<string, unknown> | undefined,
38
+ weights: doc['weights'] as Record<string, number> | undefined,
39
+ default_language: doc['default_language'] as string | undefined,
40
+ language_override: doc['language_override'] as string | undefined,
41
+ });
42
+ }
43
+
44
+ function parseValidator(options: Document): MongoSchemaValidator | undefined {
45
+ const validator = options['validator'] as Record<string, unknown> | undefined;
46
+ if (!validator) return undefined;
47
+
48
+ const jsonSchema = validator['$jsonSchema'] as Record<string, unknown> | undefined;
49
+ if (!jsonSchema) return undefined;
50
+
51
+ return new MongoSchemaValidator({
52
+ jsonSchema,
53
+ validationLevel: (options['validationLevel'] as 'strict' | 'moderate') ?? 'strict',
54
+ validationAction: (options['validationAction'] as 'error' | 'warn') ?? 'error',
55
+ });
56
+ }
57
+
58
+ function parseCollectionOptions(info: Document): MongoSchemaCollectionOptions | undefined {
59
+ const options = info['options'] as Record<string, unknown> | undefined;
60
+ if (!options) return undefined;
61
+
62
+ const capped = options['capped'] as boolean | undefined;
63
+ const size = options['size'] as number | undefined;
64
+ const max = options['max'] as number | undefined;
65
+ const timeseries = options['timeseries'] as
66
+ | { timeField: string; metaField?: string; granularity?: 'seconds' | 'minutes' | 'hours' }
67
+ | undefined;
68
+ const collation = options['collation'] as Record<string, unknown> | undefined;
69
+ const changeStreamPreAndPostImages = options['changeStreamPreAndPostImages'] as
70
+ | { enabled: boolean }
71
+ | undefined;
72
+ const clusteredIndex = options['clusteredIndex'] as { name?: string } | undefined;
73
+
74
+ const hasMeaningfulOptions =
75
+ capped || timeseries || collation || changeStreamPreAndPostImages || clusteredIndex;
76
+ if (!hasMeaningfulOptions) return undefined;
77
+
78
+ return new MongoSchemaCollectionOptions({
79
+ ...(capped ? { capped: { size: size ?? 0, ...(max != null ? { max } : {}) } } : {}),
80
+ ...(timeseries ? { timeseries } : {}),
81
+ ...(collation ? { collation } : {}),
82
+ ...(changeStreamPreAndPostImages ? { changeStreamPreAndPostImages } : {}),
83
+ ...(clusteredIndex ? { clusteredIndex } : {}),
84
+ });
85
+ }
86
+
87
+ export async function introspectSchema(db: Db): Promise<MongoSchemaIR> {
88
+ const collectionInfos = await db.listCollections().toArray();
89
+
90
+ const collections: MongoSchemaCollection[] = [];
91
+
92
+ for (const info of collectionInfos) {
93
+ const name = info['name'] as string;
94
+ const type = info['type'] as string | undefined;
95
+
96
+ if (name === PRISMA_MIGRATIONS_COLLECTION) continue;
97
+ if (name.startsWith('system.')) continue;
98
+ if (type === 'view') continue;
99
+
100
+ const indexDocs = await db.collection(name).listIndexes().toArray();
101
+ const indexes = indexDocs.filter((doc) => !isDefaultIdIndex(doc)).map(parseIndex);
102
+
103
+ const infoOptions = 'options' in info ? (info['options'] as Record<string, unknown>) : {};
104
+ const validator = parseValidator(infoOptions);
105
+ const options = parseCollectionOptions(info);
106
+
107
+ collections.push(
108
+ new MongoSchemaCollection({
109
+ name,
110
+ indexes,
111
+ ...(validator ? { validator } : {}),
112
+ ...(options ? { options } : {}),
113
+ }),
114
+ );
115
+ }
116
+
117
+ return new MongoSchemaIR(collections);
118
+ }
@@ -0,0 +1,30 @@
1
+ import type { ControlDriverInstance } from '@prisma-next/framework-components/control';
2
+ import type { Db, MongoClient } from 'mongodb';
3
+
4
+ export interface MongoControlDriverInstance extends ControlDriverInstance<'mongo', 'mongo'> {
5
+ readonly db: Db;
6
+ }
7
+
8
+ class MongoControlDriverImpl implements MongoControlDriverInstance {
9
+ readonly familyId = 'mongo' as const;
10
+ readonly targetId = 'mongo' as const;
11
+ readonly db: Db;
12
+ readonly #client: MongoClient;
13
+
14
+ constructor(db: Db, client: MongoClient) {
15
+ this.db = db;
16
+ this.#client = client;
17
+ }
18
+
19
+ query(): Promise<never> {
20
+ throw new Error('MongoDB control driver does not support SQL queries');
21
+ }
22
+
23
+ async close(): Promise<void> {
24
+ await this.#client.close();
25
+ }
26
+ }
27
+
28
+ export function createMongoControlDriver(db: Db, client: MongoClient): MongoControlDriverInstance {
29
+ return new MongoControlDriverImpl(db, client);
30
+ }