@prisma-next/mongo-schema-ir 0.0.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 ADDED
@@ -0,0 +1,58 @@
1
+ # @prisma-next/mongo-schema-ir
2
+
3
+ MongoDB Schema Intermediate Representation (IR) for migration diffing.
4
+
5
+ ## Overview
6
+
7
+ This package defines the in-memory representation of MongoDB collection schemas used by the migration planner to diff desired vs. actual state. It provides an immutable AST of collections, indexes, validators, and collection options, plus comparison utilities for index equivalence.
8
+
9
+ ## Responsibilities
10
+
11
+ - **Schema AST nodes**: `MongoSchemaIR` (root), `MongoSchemaCollection`, `MongoSchemaIndex`, `MongoSchemaValidator`, `MongoSchemaCollectionOptions` — frozen, visitable AST nodes representing MongoDB schema elements. `MongoSchemaIR` is the root node holding collections as sorted children with name-based lookup via `collection(name)`.
12
+ - **Index equivalence**: `indexesEquivalent()` compares two `MongoSchemaIndex` nodes field-by-field (keys, direction, unique, sparse, TTL, partial filter, wildcardProjection, collation, weights, default_language, language_override). Used by the planner to decide create/drop operations.
13
+ - **Deep equality**: `deepEqual()` provides key-order-sensitive structural comparison for MongoDB values. For key-order-independent comparison, use `canonicalize()`.
14
+ - **Canonical serialization**: `canonicalize()` produces a key-order-independent string representation of values. Used by the planner for index lookup keys.
15
+ - **Visitor pattern**: `MongoSchemaVisitor<R>` enables extensible traversal without modifying AST nodes.
16
+
17
+ ## Dependencies
18
+
19
+ - **`@prisma-next/mongo-contract`**: `MongoIndexKey` type for index key definitions.
20
+
21
+ **Dependents:**
22
+
23
+ - `@prisma-next/adapter-mongo` — uses the schema IR via `contractToMongoSchemaIR()` for contract-to-schema conversion, migration planning, and filter evaluation.
24
+
25
+ ## Usage
26
+
27
+ ```typescript
28
+ import {
29
+ MongoSchemaIR,
30
+ MongoSchemaCollection,
31
+ MongoSchemaIndex,
32
+ indexesEquivalent,
33
+ } from '@prisma-next/mongo-schema-ir';
34
+
35
+ const index = new MongoSchemaIndex({
36
+ keys: [{ field: 'email', direction: 1 }],
37
+ unique: true,
38
+ });
39
+
40
+ const collection = new MongoSchemaCollection({
41
+ name: 'users',
42
+ indexes: [index],
43
+ });
44
+
45
+ const ir = new MongoSchemaIR([collection]);
46
+
47
+ ir.collection('users'); // MongoSchemaCollection
48
+ ir.collectionNames; // ['users']
49
+ ir.collections; // sorted ReadonlyArray<MongoSchemaCollection>
50
+
51
+ indexesEquivalent(index, index); // true
52
+ ```
53
+
54
+ ## Architecture
55
+
56
+ - **Domain**: `mongo`
57
+ - **Layer**: `tooling`
58
+ - **Plane**: `shared` (migration-plane)
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@prisma-next/mongo-schema-ir",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "MongoDB schema IR AST for migration diffing",
7
+ "scripts": {
8
+ "build": "tsdown",
9
+ "test": "vitest run --passWithNoTests",
10
+ "test:coverage": "vitest run --coverage --passWithNoTests",
11
+ "typecheck": "tsc --project tsconfig.json --noEmit",
12
+ "lint": "biome check . --error-on-warnings",
13
+ "lint:fix": "biome check --write .",
14
+ "lint:fix:unsafe": "biome check --write --unsafe .",
15
+ "clean": "rm -rf dist dist-tsc dist-tsc-prod coverage .tmp-output"
16
+ },
17
+ "dependencies": {
18
+ "@prisma-next/mongo-contract": "workspace:*"
19
+ },
20
+ "devDependencies": {
21
+ "@prisma-next/test-utils": "workspace:*",
22
+ "@prisma-next/tsconfig": "workspace:*",
23
+ "@prisma-next/tsdown": "workspace:*",
24
+ "tsdown": "catalog:",
25
+ "typescript": "catalog:",
26
+ "vitest": "catalog:"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "src"
31
+ ],
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/exports/index.d.mts",
35
+ "import": "./dist/exports/index.mjs"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/prisma/prisma-next.git",
42
+ "directory": "packages/2-mongo-family/3-tooling/mongo-schema-ir"
43
+ }
44
+ }
@@ -0,0 +1,10 @@
1
+ export function canonicalize(obj: unknown): string {
2
+ if (obj === null) return 'null';
3
+ if (obj === undefined) return 'undefined';
4
+ if (typeof obj !== 'object') return JSON.stringify(obj);
5
+ if (Array.isArray(obj)) return `[${obj.map(canonicalize).join(',')}]`;
6
+ const record = obj as Record<string, unknown>;
7
+ const sorted = Object.keys(record).sort();
8
+ const entries = sorted.map((k) => `${JSON.stringify(k)}:${canonicalize(record[k])}`);
9
+ return `{${entries.join(',')}}`;
10
+ }
@@ -0,0 +1,14 @@
1
+ export { canonicalize } from '../canonicalize';
2
+ export { deepEqual, indexesEquivalent } from '../index-equivalence';
3
+ export type { MongoSchemaCollectionCtorOptions } from '../schema-collection';
4
+ export { MongoSchemaCollection } from '../schema-collection';
5
+ export type { MongoSchemaCollectionOptionsInput } from '../schema-collection-options';
6
+ export { MongoSchemaCollectionOptions } from '../schema-collection-options';
7
+ export type { MongoSchemaIndexOptions } from '../schema-index';
8
+ export { MongoSchemaIndex } from '../schema-index';
9
+ export { MongoSchemaIR } from '../schema-ir';
10
+ export { MongoSchemaNode } from '../schema-node';
11
+ export type { MongoSchemaValidatorOptions } from '../schema-validator';
12
+ export { MongoSchemaValidator } from '../schema-validator';
13
+ export type { AnyMongoSchemaNode } from '../types';
14
+ export type { MongoSchemaVisitor } from '../visitor';
@@ -0,0 +1,60 @@
1
+ import { canonicalize } from './canonicalize';
2
+ import type { MongoSchemaIndex } from './schema-index';
3
+
4
+ /**
5
+ * Key-order-sensitive structural comparison. For key-order-independent
6
+ * comparison (e.g. lookup key construction), use {@link canonicalize}.
7
+ */
8
+ export function deepEqual(a: unknown, b: unknown): boolean {
9
+ if (a === b) return true;
10
+ if (a === null || b === null) return false;
11
+ if (a === undefined || b === undefined) return false;
12
+ if (typeof a !== typeof b) return false;
13
+
14
+ if (Array.isArray(a)) {
15
+ if (!Array.isArray(b)) return false;
16
+ if (a.length !== b.length) return false;
17
+ for (let i = 0; i < a.length; i++) {
18
+ if (!deepEqual(a[i], b[i])) return false;
19
+ }
20
+ return true;
21
+ }
22
+
23
+ if (typeof a === 'object' && typeof b === 'object') {
24
+ const aObj = a as Record<string, unknown>;
25
+ const bObj = b as Record<string, unknown>;
26
+ const aKeys = Object.keys(aObj);
27
+ const bKeys = Object.keys(bObj);
28
+ if (aKeys.length !== bKeys.length) return false;
29
+ for (let i = 0; i < aKeys.length; i++) {
30
+ if (aKeys[i] !== bKeys[i]) return false;
31
+ const key = aKeys[i] as string;
32
+ if (!deepEqual(aObj[key], bObj[key])) return false;
33
+ }
34
+ return true;
35
+ }
36
+
37
+ return false;
38
+ }
39
+
40
+ export function indexesEquivalent(a: MongoSchemaIndex, b: MongoSchemaIndex): boolean {
41
+ if (a.keys.length !== b.keys.length) return false;
42
+ for (let i = 0; i < a.keys.length; i++) {
43
+ const aKey = a.keys[i];
44
+ const bKey = b.keys[i];
45
+ if (!aKey || !bKey) return false;
46
+ if (aKey.field !== bKey.field) return false;
47
+ if (aKey.direction !== bKey.direction) return false;
48
+ }
49
+ if (a.unique !== b.unique) return false;
50
+ if (a.sparse !== b.sparse) return false;
51
+ if (a.expireAfterSeconds !== b.expireAfterSeconds) return false;
52
+ if (canonicalize(a.partialFilterExpression) !== canonicalize(b.partialFilterExpression))
53
+ return false;
54
+ if (canonicalize(a.wildcardProjection) !== canonicalize(b.wildcardProjection)) return false;
55
+ if (canonicalize(a.collation) !== canonicalize(b.collation)) return false;
56
+ if (canonicalize(a.weights) !== canonicalize(b.weights)) return false;
57
+ if (a.default_language !== b.default_language) return false;
58
+ if (a.language_override !== b.language_override) return false;
59
+ return true;
60
+ }
@@ -0,0 +1,39 @@
1
+ import { MongoSchemaNode } from './schema-node';
2
+ import type { MongoSchemaVisitor } from './visitor';
3
+
4
+ export interface MongoSchemaCollectionOptionsInput {
5
+ readonly capped?: { size: number; max?: number };
6
+ readonly timeseries?: {
7
+ timeField: string;
8
+ metaField?: string;
9
+ granularity?: 'seconds' | 'minutes' | 'hours';
10
+ };
11
+ readonly collation?: Record<string, unknown>;
12
+ readonly changeStreamPreAndPostImages?: { enabled: boolean };
13
+ readonly clusteredIndex?: { name?: string };
14
+ }
15
+
16
+ export class MongoSchemaCollectionOptions extends MongoSchemaNode {
17
+ readonly kind = 'collectionOptions' as const;
18
+ readonly capped?: { size: number; max?: number } | undefined;
19
+ readonly timeseries?:
20
+ | { timeField: string; metaField?: string; granularity?: 'seconds' | 'minutes' | 'hours' }
21
+ | undefined;
22
+ readonly collation?: Record<string, unknown> | undefined;
23
+ readonly changeStreamPreAndPostImages?: { enabled: boolean } | undefined;
24
+ readonly clusteredIndex?: { name?: string } | undefined;
25
+
26
+ constructor(options: MongoSchemaCollectionOptionsInput) {
27
+ super();
28
+ this.capped = options.capped;
29
+ this.timeseries = options.timeseries;
30
+ this.collation = options.collation;
31
+ this.changeStreamPreAndPostImages = options.changeStreamPreAndPostImages;
32
+ this.clusteredIndex = options.clusteredIndex;
33
+ this.freeze();
34
+ }
35
+
36
+ accept<R>(visitor: MongoSchemaVisitor<R>): R {
37
+ return visitor.collectionOptions(this);
38
+ }
39
+ }
@@ -0,0 +1,33 @@
1
+ import type { MongoSchemaCollectionOptions } from './schema-collection-options';
2
+ import type { MongoSchemaIndex } from './schema-index';
3
+ import { MongoSchemaNode } from './schema-node';
4
+ import type { MongoSchemaValidator } from './schema-validator';
5
+ import type { MongoSchemaVisitor } from './visitor';
6
+
7
+ export interface MongoSchemaCollectionCtorOptions {
8
+ readonly name: string;
9
+ readonly indexes?: ReadonlyArray<MongoSchemaIndex>;
10
+ readonly validator?: MongoSchemaValidator;
11
+ readonly options?: MongoSchemaCollectionOptions;
12
+ }
13
+
14
+ export class MongoSchemaCollection extends MongoSchemaNode {
15
+ readonly kind = 'collection' as const;
16
+ readonly name: string;
17
+ readonly indexes: ReadonlyArray<MongoSchemaIndex>;
18
+ readonly validator?: MongoSchemaValidator | undefined;
19
+ readonly options?: MongoSchemaCollectionOptions | undefined;
20
+
21
+ constructor(options: MongoSchemaCollectionCtorOptions) {
22
+ super();
23
+ this.name = options.name;
24
+ this.indexes = options.indexes ?? [];
25
+ this.validator = options.validator;
26
+ this.options = options.options;
27
+ this.freeze();
28
+ }
29
+
30
+ accept<R>(visitor: MongoSchemaVisitor<R>): R {
31
+ return visitor.collection(this);
32
+ }
33
+ }
@@ -0,0 +1,49 @@
1
+ import type { MongoIndexKey } from '@prisma-next/mongo-contract';
2
+ import { MongoSchemaNode } from './schema-node';
3
+ import type { MongoSchemaVisitor } from './visitor';
4
+
5
+ export interface MongoSchemaIndexOptions {
6
+ readonly keys: ReadonlyArray<MongoIndexKey>;
7
+ readonly unique?: boolean | undefined;
8
+ readonly sparse?: boolean | undefined;
9
+ readonly expireAfterSeconds?: number | undefined;
10
+ readonly partialFilterExpression?: Record<string, unknown> | undefined;
11
+ readonly wildcardProjection?: Record<string, 0 | 1> | undefined;
12
+ readonly collation?: Record<string, unknown> | undefined;
13
+ readonly weights?: Record<string, number> | undefined;
14
+ readonly default_language?: string | undefined;
15
+ readonly language_override?: string | undefined;
16
+ }
17
+
18
+ export class MongoSchemaIndex extends MongoSchemaNode {
19
+ readonly kind = 'index' as const;
20
+ readonly keys: ReadonlyArray<MongoIndexKey>;
21
+ readonly unique: boolean;
22
+ readonly sparse?: boolean | undefined;
23
+ readonly expireAfterSeconds?: number | undefined;
24
+ readonly partialFilterExpression?: Record<string, unknown> | undefined;
25
+ readonly wildcardProjection?: Record<string, 0 | 1> | undefined;
26
+ readonly collation?: Record<string, unknown> | undefined;
27
+ readonly weights?: Record<string, number> | undefined;
28
+ readonly default_language?: string | undefined;
29
+ readonly language_override?: string | undefined;
30
+
31
+ constructor(options: MongoSchemaIndexOptions) {
32
+ super();
33
+ this.keys = options.keys;
34
+ this.unique = options.unique ?? false;
35
+ this.sparse = options.sparse;
36
+ this.expireAfterSeconds = options.expireAfterSeconds;
37
+ this.partialFilterExpression = options.partialFilterExpression;
38
+ this.wildcardProjection = options.wildcardProjection;
39
+ this.collation = options.collation;
40
+ this.weights = options.weights;
41
+ this.default_language = options.default_language;
42
+ this.language_override = options.language_override;
43
+ this.freeze();
44
+ }
45
+
46
+ accept<R>(visitor: MongoSchemaVisitor<R>): R {
47
+ return visitor.index(this);
48
+ }
49
+ }
@@ -0,0 +1,28 @@
1
+ import type { MongoSchemaCollection } from './schema-collection';
2
+ import { MongoSchemaNode } from './schema-node';
3
+ import type { MongoSchemaVisitor } from './visitor';
4
+
5
+ export class MongoSchemaIR extends MongoSchemaNode {
6
+ readonly kind = 'schema' as const;
7
+ readonly collections: ReadonlyArray<MongoSchemaCollection>;
8
+ readonly collectionNames: ReadonlyArray<string>;
9
+
10
+ private readonly _byName: Map<string, MongoSchemaCollection>;
11
+
12
+ constructor(collections: ReadonlyArray<MongoSchemaCollection>) {
13
+ super();
14
+ const sorted = [...collections].sort((a, b) => a.name.localeCompare(b.name));
15
+ this.collections = sorted;
16
+ this._byName = new Map(sorted.map((c) => [c.name, c]));
17
+ this.collectionNames = sorted.map((c) => c.name);
18
+ this.freeze();
19
+ }
20
+
21
+ accept<R>(visitor: MongoSchemaVisitor<R>): R {
22
+ return visitor.schema(this);
23
+ }
24
+
25
+ collection(name: string): MongoSchemaCollection | undefined {
26
+ return this._byName.get(name);
27
+ }
28
+ }
@@ -0,0 +1,11 @@
1
+ import type { MongoSchemaVisitor } from './visitor';
2
+
3
+ export abstract class MongoSchemaNode {
4
+ abstract readonly kind: string;
5
+
6
+ abstract accept<R>(visitor: MongoSchemaVisitor<R>): R;
7
+
8
+ protected freeze(): void {
9
+ Object.freeze(this);
10
+ }
11
+ }
@@ -0,0 +1,27 @@
1
+ import { MongoSchemaNode } from './schema-node';
2
+ import type { MongoSchemaVisitor } from './visitor';
3
+
4
+ export interface MongoSchemaValidatorOptions {
5
+ readonly jsonSchema: Record<string, unknown>;
6
+ readonly validationLevel: 'strict' | 'moderate';
7
+ readonly validationAction: 'error' | 'warn';
8
+ }
9
+
10
+ export class MongoSchemaValidator extends MongoSchemaNode {
11
+ readonly kind = 'validator' as const;
12
+ readonly jsonSchema: Record<string, unknown>;
13
+ readonly validationLevel: 'strict' | 'moderate';
14
+ readonly validationAction: 'error' | 'warn';
15
+
16
+ constructor(options: MongoSchemaValidatorOptions) {
17
+ super();
18
+ this.jsonSchema = options.jsonSchema;
19
+ this.validationLevel = options.validationLevel;
20
+ this.validationAction = options.validationAction;
21
+ this.freeze();
22
+ }
23
+
24
+ accept<R>(visitor: MongoSchemaVisitor<R>): R {
25
+ return visitor.validator(this);
26
+ }
27
+ }
package/src/types.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { MongoSchemaCollection } from './schema-collection';
2
+ import type { MongoSchemaCollectionOptions } from './schema-collection-options';
3
+ import type { MongoSchemaIndex } from './schema-index';
4
+ import type { MongoSchemaIR } from './schema-ir';
5
+ import type { MongoSchemaValidator } from './schema-validator';
6
+
7
+ export type AnyMongoSchemaNode =
8
+ | MongoSchemaIR
9
+ | MongoSchemaCollection
10
+ | MongoSchemaCollectionOptions
11
+ | MongoSchemaIndex
12
+ | MongoSchemaValidator;
package/src/visitor.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { MongoSchemaCollection } from './schema-collection';
2
+ import type { MongoSchemaCollectionOptions } from './schema-collection-options';
3
+ import type { MongoSchemaIndex } from './schema-index';
4
+ import type { MongoSchemaIR } from './schema-ir';
5
+ import type { MongoSchemaValidator } from './schema-validator';
6
+
7
+ export interface MongoSchemaVisitor<R> {
8
+ schema(node: MongoSchemaIR): R;
9
+ collection(node: MongoSchemaCollection): R;
10
+ index(node: MongoSchemaIndex): R;
11
+ validator(node: MongoSchemaValidator): R;
12
+ collectionOptions(node: MongoSchemaCollectionOptions): R;
13
+ }