@prisma-next/mongo-contract 0.11.0 → 0.12.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/dist/canonicalization-hooks.d.mts +9 -0
- package/dist/canonicalization-hooks.d.mts.map +1 -0
- package/dist/canonicalization-hooks.mjs +20 -0
- package/dist/canonicalization-hooks.mjs.map +1 -0
- package/dist/index.d.mts +20 -13
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +120 -96
- package/dist/index.mjs.map +1 -1
- package/package.json +20 -8
- package/src/canonicalization-hooks.ts +27 -0
- package/src/contract-schema.ts +16 -10
- package/src/contract-types.ts +15 -7
- package/src/exports/canonicalization-hooks.ts +1 -0
- package/src/exports/index.ts +3 -0
- package/src/ir/build-mongo-namespace.ts +80 -0
- package/src/ir/mongo-storage.ts +7 -67
- package/src/validate-storage.ts +52 -45
package/src/ir/mongo-storage.ts
CHANGED
|
@@ -3,73 +3,15 @@ import {
|
|
|
3
3
|
freezeNode,
|
|
4
4
|
IRNodeBase,
|
|
5
5
|
type Namespace,
|
|
6
|
-
NamespaceBase,
|
|
7
6
|
type Storage,
|
|
8
|
-
UNBOUND_NAMESPACE_ID,
|
|
9
7
|
} from '@prisma-next/framework-components/ir';
|
|
10
|
-
import { MongoCollection,
|
|
11
|
-
import { MongoUnboundNamespace } from './mongo-unbound-namespace';
|
|
8
|
+
import type { MongoCollection, MongoCollectionInput } from './mongo-collection';
|
|
12
9
|
|
|
13
10
|
export interface MongoNamespaceCollectionsInput {
|
|
14
11
|
readonly id: string;
|
|
15
12
|
readonly collections?: Record<string, MongoCollection | MongoCollectionInput>;
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
export interface MongoStorageInput<THash extends string = string> {
|
|
19
|
-
readonly storageHash: StorageHashBase<THash>;
|
|
20
|
-
readonly namespaces?: Readonly<Record<string, Namespace | MongoNamespaceCollectionsInput>>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const DEFAULT_NAMESPACES: Readonly<Record<string, Namespace>> = Object.freeze({
|
|
24
|
-
[UNBOUND_NAMESPACE_ID]: MongoUnboundNamespace.instance,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
class MongoNamespacePayload extends NamespaceBase {
|
|
28
|
-
declare readonly kind: string;
|
|
29
|
-
|
|
30
|
-
readonly id: string;
|
|
31
|
-
readonly collections: Readonly<Record<string, MongoCollection>>;
|
|
32
|
-
|
|
33
|
-
constructor(input: MongoNamespaceCollectionsInput) {
|
|
34
|
-
super();
|
|
35
|
-
this.id = input.id;
|
|
36
|
-
this.collections = Object.freeze(
|
|
37
|
-
Object.fromEntries(
|
|
38
|
-
Object.entries(input.collections ?? {}).map(([name, c]) => [
|
|
39
|
-
name,
|
|
40
|
-
c instanceof MongoCollection ? c : new MongoCollection(c),
|
|
41
|
-
]),
|
|
42
|
-
),
|
|
43
|
-
);
|
|
44
|
-
Object.defineProperty(this, 'kind', {
|
|
45
|
-
value: 'mongo-namespace',
|
|
46
|
-
writable: false,
|
|
47
|
-
enumerable: false,
|
|
48
|
-
configurable: true,
|
|
49
|
-
});
|
|
50
|
-
freezeNode(this);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function normaliseNamespaceEntry(
|
|
55
|
-
nsKey: string,
|
|
56
|
-
ns: Namespace | MongoNamespaceCollectionsInput,
|
|
57
|
-
): Namespace {
|
|
58
|
-
if (ns instanceof NamespaceBase) {
|
|
59
|
-
return ns;
|
|
60
|
-
}
|
|
61
|
-
// The framework `Namespace` interface only promises `id`; the remaining
|
|
62
|
-
// arm of this union — plain-object inputs accepted by `MongoStorageInput`
|
|
63
|
-
// — is `MongoNamespaceCollectionsInput`. The `instanceof` guard above
|
|
64
|
-
// discriminates the two; TypeScript can't narrow further without a hint.
|
|
65
|
-
const input = ns as MongoNamespaceCollectionsInput;
|
|
66
|
-
const collectionCount = Object.keys(input.collections ?? {}).length;
|
|
67
|
-
if (nsKey === UNBOUND_NAMESPACE_ID && collectionCount === 0) {
|
|
68
|
-
return MongoUnboundNamespace.instance;
|
|
69
|
-
}
|
|
70
|
-
return new MongoNamespacePayload(input);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
15
|
// Mongo concretions always store `MongoCollection` instances in
|
|
74
16
|
// `collections` (Mongo idiom — distinct from the SQL family's `tables`).
|
|
75
17
|
// Narrowing the namespace map here lets target/family-level consumers
|
|
@@ -80,6 +22,11 @@ export type MongoNamespace = Namespace & {
|
|
|
80
22
|
readonly collections: Readonly<Record<string, MongoCollection>>;
|
|
81
23
|
};
|
|
82
24
|
|
|
25
|
+
export interface MongoStorageInput<THash extends string = string> {
|
|
26
|
+
readonly storageHash: StorageHashBase<THash>;
|
|
27
|
+
readonly namespaces: Readonly<Record<string, MongoNamespace>>;
|
|
28
|
+
}
|
|
29
|
+
|
|
83
30
|
export class MongoStorage<THash extends string = string> extends IRNodeBase implements Storage {
|
|
84
31
|
declare readonly kind: 'mongo-storage';
|
|
85
32
|
readonly storageHash: StorageHashBase<THash>;
|
|
@@ -94,14 +41,7 @@ export class MongoStorage<THash extends string = string> extends IRNodeBase impl
|
|
|
94
41
|
configurable: true,
|
|
95
42
|
});
|
|
96
43
|
this.storageHash = input.storageHash;
|
|
97
|
-
this.namespaces = Object.freeze(
|
|
98
|
-
Object.fromEntries(
|
|
99
|
-
Object.entries(input.namespaces ?? DEFAULT_NAMESPACES).map(([nsKey, ns]) => [
|
|
100
|
-
nsKey,
|
|
101
|
-
normaliseNamespaceEntry(nsKey, ns) as MongoNamespace,
|
|
102
|
-
]),
|
|
103
|
-
),
|
|
104
|
-
);
|
|
44
|
+
this.namespaces = Object.freeze(input.namespaces);
|
|
105
45
|
freezeNode(this);
|
|
106
46
|
}
|
|
107
47
|
}
|
package/src/validate-storage.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type { MongoContract } from './contract-types';
|
|
1
|
+
import type { MongoContract, MongoModelDefinition } from './contract-types';
|
|
2
|
+
|
|
3
|
+
function formatCrossRef(crossRef: { readonly namespace: string; readonly model: string }): string {
|
|
4
|
+
return `${crossRef.namespace}.${crossRef.model}`;
|
|
5
|
+
}
|
|
2
6
|
|
|
3
7
|
function storageDeclaresCollection(
|
|
4
8
|
storage: MongoContract['storage'],
|
|
@@ -15,62 +19,65 @@ function storageDeclaresCollection(
|
|
|
15
19
|
export function validateMongoStorage(contract: MongoContract): void {
|
|
16
20
|
const errors: string[] = [];
|
|
17
21
|
|
|
18
|
-
for (const [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
|
|
22
|
+
for (const [namespaceId, namespace] of Object.entries(contract.domain.namespaces)) {
|
|
23
|
+
const models = namespace.models as Record<string, MongoModelDefinition>;
|
|
24
|
+
for (const [modelName, model] of Object.entries(models)) {
|
|
25
|
+
const qualifiedName = `${namespaceId}:${modelName}`;
|
|
26
|
+
if (
|
|
27
|
+
model.storage.collection &&
|
|
28
|
+
!storageDeclaresCollection(contract.storage, model.storage.collection)
|
|
29
|
+
) {
|
|
30
|
+
errors.push(
|
|
31
|
+
`Model "${qualifiedName}" references collection "${model.storage.collection}" which is not declared under any namespace's collections map`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
);
|
|
35
|
+
if (model.base) {
|
|
36
|
+
const baseModel = models[model.base.model];
|
|
37
|
+
if (baseModel) {
|
|
38
|
+
const variantCollection = model.storage.collection;
|
|
39
|
+
const baseCollection = baseModel.storage.collection;
|
|
40
|
+
if (variantCollection !== baseCollection) {
|
|
41
|
+
errors.push(
|
|
42
|
+
`Mongo does not support multi-table inheritance; variant "${qualifiedName}" must share its base's collection ("${baseCollection ?? '(none)'}"), but has "${variantCollection ?? '(none)'}"`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
|
-
}
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
for (const [relName, relation] of Object.entries(model.relations ?? {})) {
|
|
49
|
+
const targetModel = models[relation.to.model];
|
|
50
|
+
const targetLabel = formatCrossRef(relation.to);
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
errors.push(
|
|
49
|
-
`Embed relation "${relName}" targets "${relation.to}" which is owned by "${targetModel.owner}", not "${modelName}"`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
if (targetModel.storage.collection) {
|
|
53
|
-
errors.push(
|
|
54
|
-
`Embed relation "${relName}" targets "${relation.to}" which must not have a collection`,
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
} else if ('on' in relation && relation.on) {
|
|
58
|
-
for (const localField of relation.on.localFields) {
|
|
59
|
-
if (!(localField in model.fields)) {
|
|
52
|
+
if (targetModel?.owner) {
|
|
53
|
+
if (targetModel.owner !== modelName) {
|
|
60
54
|
errors.push(
|
|
61
|
-
`
|
|
55
|
+
`Embed relation "${relName}" targets "${targetLabel}" which is owned by "${targetModel.owner}", not "${qualifiedName}"`,
|
|
62
56
|
);
|
|
63
57
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
if (targetModel.storage.collection) {
|
|
59
|
+
errors.push(
|
|
60
|
+
`Embed relation "${relName}" targets "${targetLabel}" which must not have a collection`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
} else if ('on' in relation && relation.on) {
|
|
64
|
+
for (const localField of relation.on.localFields) {
|
|
65
|
+
if (!(localField in model.fields)) {
|
|
69
66
|
errors.push(
|
|
70
|
-
`Reference relation "${relName}":
|
|
67
|
+
`Reference relation "${relName}": localField "${localField}" is not a field on model "${qualifiedName}"`,
|
|
71
68
|
);
|
|
72
69
|
}
|
|
73
70
|
}
|
|
71
|
+
|
|
72
|
+
if (targetModel) {
|
|
73
|
+
for (const targetField of relation.on.targetFields) {
|
|
74
|
+
if (!(targetField in targetModel.fields)) {
|
|
75
|
+
errors.push(
|
|
76
|
+
`Reference relation "${relName}": targetField "${targetField}" is not a field on model "${targetLabel}"`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
}
|