@prisma-next/sql-contract 0.8.0 → 0.9.0-dev.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 +13 -11
- package/dist/factories.d.mts +2 -2
- package/dist/factories.d.mts.map +1 -1
- package/dist/factories.mjs +16 -14
- package/dist/factories.mjs.map +1 -1
- package/dist/index-type-validation.d.mts +2 -2
- package/dist/index-type-validation.mjs +1 -1
- package/dist/index-type-validation.mjs.map +1 -1
- package/dist/{index-types-DqVqGHwg.d.mts → index-types-B1cf5N0F.d.mts} +1 -1
- package/dist/{index-types-DqVqGHwg.d.mts.map → index-types-B1cf5N0F.d.mts.map} +1 -1
- package/dist/index-types.d.mts +1 -1
- package/dist/types-B0lbr9cb.d.mts +498 -0
- package/dist/types-B0lbr9cb.d.mts.map +1 -0
- package/dist/types-iqFGDcJp.mjs +389 -0
- package/dist/types-iqFGDcJp.mjs.map +1 -0
- package/dist/types.d.mts +2 -2
- package/dist/types.mjs +2 -2
- package/dist/validators.d.mts +27 -15
- package/dist/validators.d.mts.map +1 -1
- package/dist/validators.mjs +409 -2
- package/dist/validators.mjs.map +1 -0
- package/package.json +6 -7
- package/src/exports/types.ts +30 -9
- package/src/factories.ts +19 -32
- package/src/index-type-validation.ts +1 -1
- package/src/index.ts +0 -1
- package/src/ir/foreign-key-references.ts +26 -0
- package/src/ir/foreign-key.ts +50 -0
- package/src/ir/postgres-enum-storage-entry.ts +55 -0
- package/src/ir/primary-key.ts +22 -0
- package/src/ir/sql-index.ts +33 -0
- package/src/ir/sql-node.ts +52 -0
- package/src/ir/sql-storage.ts +154 -0
- package/src/ir/sql-unspecified-namespace.ts +51 -0
- package/src/ir/storage-column.ts +55 -0
- package/src/ir/storage-table.ts +63 -0
- package/src/ir/storage-type-instance.ts +60 -0
- package/src/ir/unique-constraint.ts +22 -0
- package/src/types.ts +38 -99
- package/src/validators.ts +268 -28
- package/dist/types-hgzy8ME1.mjs +0 -13
- package/dist/types-hgzy8ME1.mjs.map +0 -1
- package/dist/types-njsiV-Ck.d.mts +0 -186
- package/dist/types-njsiV-Ck.d.mts.map +0 -1
- package/dist/validate.d.mts +0 -9
- package/dist/validate.d.mts.map +0 -1
- package/dist/validate.mjs +0 -106
- package/dist/validate.mjs.map +0 -1
- package/dist/validators-Dm5X-Hvg.mjs +0 -294
- package/dist/validators-Dm5X-Hvg.mjs.map +0 -1
- package/src/exports/validate.ts +0 -1
- package/src/validate.ts +0 -227
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
2
|
+
import { ForeignKey, type ForeignKeyInput } from './foreign-key';
|
|
3
|
+
import { PrimaryKey, type PrimaryKeyInput } from './primary-key';
|
|
4
|
+
import { Index, type IndexInput } from './sql-index';
|
|
5
|
+
import { SqlNode } from './sql-node';
|
|
6
|
+
import { StorageColumn, type StorageColumnInput } from './storage-column';
|
|
7
|
+
import { UniqueConstraint, type UniqueConstraintInput } from './unique-constraint';
|
|
8
|
+
|
|
9
|
+
export interface StorageTableInput {
|
|
10
|
+
readonly columns: Record<string, StorageColumn | StorageColumnInput>;
|
|
11
|
+
readonly primaryKey?: PrimaryKey | PrimaryKeyInput;
|
|
12
|
+
readonly uniques: ReadonlyArray<UniqueConstraint | UniqueConstraintInput>;
|
|
13
|
+
readonly indexes: ReadonlyArray<Index | IndexInput>;
|
|
14
|
+
readonly foreignKeys: ReadonlyArray<ForeignKey | ForeignKeyInput>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* SQL Contract IR node for a single table entry in `SqlStorage.tables`.
|
|
19
|
+
*
|
|
20
|
+
* The constructor normalises nested IR-class fields (columns, primary
|
|
21
|
+
* key, uniques, indexes, foreign keys) into the appropriate class
|
|
22
|
+
* instances so downstream walks see a uniform AST regardless of whether
|
|
23
|
+
* the input was a JSON literal or an already-constructed class.
|
|
24
|
+
*
|
|
25
|
+
* The table's `name` is not on the class — tables are keyed by name in
|
|
26
|
+
* the parent `SqlStorage.tables: Record<string, StorageTable>` map.
|
|
27
|
+
* A future namespace-aware milestone will add a `namespaceId` field
|
|
28
|
+
* when namespace-keyed storage lands; today's single-namespace shape
|
|
29
|
+
* needs neither field.
|
|
30
|
+
*/
|
|
31
|
+
export class StorageTable extends SqlNode {
|
|
32
|
+
readonly columns: Readonly<Record<string, StorageColumn>>;
|
|
33
|
+
readonly uniques: ReadonlyArray<UniqueConstraint>;
|
|
34
|
+
readonly indexes: ReadonlyArray<Index>;
|
|
35
|
+
readonly foreignKeys: ReadonlyArray<ForeignKey>;
|
|
36
|
+
declare readonly primaryKey?: PrimaryKey;
|
|
37
|
+
|
|
38
|
+
constructor(input: StorageTableInput) {
|
|
39
|
+
super();
|
|
40
|
+
this.columns = Object.freeze(
|
|
41
|
+
Object.fromEntries(
|
|
42
|
+
Object.entries(input.columns).map(([name, col]) => [
|
|
43
|
+
name,
|
|
44
|
+
col instanceof StorageColumn ? col : new StorageColumn(col),
|
|
45
|
+
]),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
if (input.primaryKey !== undefined) {
|
|
49
|
+
this.primaryKey =
|
|
50
|
+
input.primaryKey instanceof PrimaryKey
|
|
51
|
+
? input.primaryKey
|
|
52
|
+
: new PrimaryKey(input.primaryKey);
|
|
53
|
+
}
|
|
54
|
+
this.uniques = Object.freeze(
|
|
55
|
+
input.uniques.map((u) => (u instanceof UniqueConstraint ? u : new UniqueConstraint(u))),
|
|
56
|
+
);
|
|
57
|
+
this.indexes = Object.freeze(input.indexes.map((i) => (i instanceof Index ? i : new Index(i))));
|
|
58
|
+
this.foreignKeys = Object.freeze(
|
|
59
|
+
input.foreignKeys.map((fk) => (fk instanceof ForeignKey ? fk : new ForeignKey(fk))),
|
|
60
|
+
);
|
|
61
|
+
freezeNode(this);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { StorageType } from '@prisma-next/framework-components/ir';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sentinel kind for the legacy codec-triple shape persisted under
|
|
5
|
+
* `SqlStorage.types`. Plain JSON-clean object literals carry this
|
|
6
|
+
* discriminator so the polymorphic slot dispatch can route them down
|
|
7
|
+
* the codec path while target-specific IR class instances (e.g. the
|
|
8
|
+
* Postgres enum class) keep their own narrower `kind` literal.
|
|
9
|
+
*/
|
|
10
|
+
export const CODEC_INSTANCE_KIND = 'codec-instance' as const;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Structural sub-interface of {@link StorageType} for codec-typed entries
|
|
14
|
+
* in `SqlStorage.types`. These are plain object literals — there is no
|
|
15
|
+
* runtime IR class, the JSON envelope round-trips through the slot
|
|
16
|
+
* unchanged. The `kind: 'codec-instance'` discriminator is the dispatch
|
|
17
|
+
* key that distinguishes codec-typed entries from class-instance entries
|
|
18
|
+
* (e.g. `PostgresEnumType`) sharing the polymorphic slot.
|
|
19
|
+
*/
|
|
20
|
+
export interface StorageTypeInstance extends StorageType {
|
|
21
|
+
readonly kind: typeof CODEC_INSTANCE_KIND;
|
|
22
|
+
readonly codecId: string;
|
|
23
|
+
readonly nativeType: string;
|
|
24
|
+
readonly typeParams: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Construction-time input for a codec-triple entry. Symmetric with the
|
|
29
|
+
* structural runtime shape minus the `kind` discriminator — callers may
|
|
30
|
+
* omit `kind`; the helper {@link toStorageTypeInstance} stamps it on.
|
|
31
|
+
*/
|
|
32
|
+
export interface StorageTypeInstanceInput {
|
|
33
|
+
readonly codecId: string;
|
|
34
|
+
readonly nativeType: string;
|
|
35
|
+
readonly typeParams: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stamp the codec-instance `kind` discriminator on a caller-supplied
|
|
40
|
+
* codec triple. Idempotent: input that already carries the discriminator
|
|
41
|
+
* passes through unchanged.
|
|
42
|
+
*/
|
|
43
|
+
export function toStorageTypeInstance(input: StorageTypeInstanceInput): StorageTypeInstance {
|
|
44
|
+
return {
|
|
45
|
+
kind: CODEC_INSTANCE_KIND,
|
|
46
|
+
codecId: input.codecId,
|
|
47
|
+
nativeType: input.nativeType,
|
|
48
|
+
typeParams: input.typeParams,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Type-guard for codec-typed entries on the polymorphic
|
|
54
|
+
* `SqlStorage.types` slot. Distinguishes `StorageTypeInstance` from
|
|
55
|
+
* class-instance kinds (e.g. `PostgresEnumType`).
|
|
56
|
+
*/
|
|
57
|
+
export function isStorageTypeInstance(value: unknown): value is StorageTypeInstance {
|
|
58
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
59
|
+
return (value as { kind?: unknown }).kind === CODEC_INSTANCE_KIND;
|
|
60
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
2
|
+
import { SqlNode } from './sql-node';
|
|
3
|
+
|
|
4
|
+
export interface UniqueConstraintInput {
|
|
5
|
+
readonly columns: readonly string[];
|
|
6
|
+
readonly name?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* SQL Contract IR node for a table-level unique constraint.
|
|
11
|
+
*/
|
|
12
|
+
export class UniqueConstraint extends SqlNode {
|
|
13
|
+
readonly columns: readonly string[];
|
|
14
|
+
declare readonly name?: string;
|
|
15
|
+
|
|
16
|
+
constructor(input: UniqueConstraintInput) {
|
|
17
|
+
super();
|
|
18
|
+
this.columns = input.columns;
|
|
19
|
+
if (input.name !== undefined) this.name = input.name;
|
|
20
|
+
freezeNode(this);
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,58 +1,42 @@
|
|
|
1
|
-
import type { ColumnDefault, StorageBase } from '@prisma-next/contract/types';
|
|
2
1
|
import type { CodecTrait } from '@prisma-next/framework-components/codec';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export type Index = {
|
|
44
|
-
readonly columns: readonly string[];
|
|
45
|
-
readonly name?: string;
|
|
46
|
-
readonly type?: string;
|
|
47
|
-
readonly options?: Record<string, unknown>;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export type ForeignKeyReferences = {
|
|
51
|
-
readonly table: string;
|
|
52
|
-
readonly columns: readonly string[];
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export type ReferentialAction = 'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault';
|
|
2
|
+
import type { ReferentialAction } from './ir/foreign-key';
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
ForeignKey,
|
|
6
|
+
type ForeignKeyInput,
|
|
7
|
+
type ReferentialAction,
|
|
8
|
+
} from './ir/foreign-key';
|
|
9
|
+
export {
|
|
10
|
+
ForeignKeyReferences,
|
|
11
|
+
type ForeignKeyReferencesInput,
|
|
12
|
+
} from './ir/foreign-key-references';
|
|
13
|
+
export {
|
|
14
|
+
isPostgresEnumStorageEntry,
|
|
15
|
+
POSTGRES_ENUM_KIND,
|
|
16
|
+
type PostgresEnumStorageEntry,
|
|
17
|
+
} from './ir/postgres-enum-storage-entry';
|
|
18
|
+
export { PrimaryKey, type PrimaryKeyInput } from './ir/primary-key';
|
|
19
|
+
export { Index, type IndexInput } from './ir/sql-index';
|
|
20
|
+
export { SqlNode } from './ir/sql-node';
|
|
21
|
+
export {
|
|
22
|
+
SqlStorage,
|
|
23
|
+
type SqlStorageInput,
|
|
24
|
+
type SqlStorageTypeEntry,
|
|
25
|
+
} from './ir/sql-storage';
|
|
26
|
+
export { SqlUnspecifiedNamespace } from './ir/sql-unspecified-namespace';
|
|
27
|
+
export { StorageColumn, type StorageColumnInput } from './ir/storage-column';
|
|
28
|
+
export { StorageTable, type StorageTableInput } from './ir/storage-table';
|
|
29
|
+
export {
|
|
30
|
+
CODEC_INSTANCE_KIND,
|
|
31
|
+
isStorageTypeInstance,
|
|
32
|
+
type StorageTypeInstance,
|
|
33
|
+
type StorageTypeInstanceInput,
|
|
34
|
+
toStorageTypeInstance,
|
|
35
|
+
} from './ir/storage-type-instance';
|
|
36
|
+
export {
|
|
37
|
+
UniqueConstraint,
|
|
38
|
+
type UniqueConstraintInput,
|
|
39
|
+
} from './ir/unique-constraint';
|
|
56
40
|
|
|
57
41
|
export type ForeignKeyOptions = {
|
|
58
42
|
readonly name?: string;
|
|
@@ -60,51 +44,6 @@ export type ForeignKeyOptions = {
|
|
|
60
44
|
readonly onUpdate?: ReferentialAction;
|
|
61
45
|
};
|
|
62
46
|
|
|
63
|
-
export type ForeignKey = {
|
|
64
|
-
readonly columns: readonly string[];
|
|
65
|
-
readonly references: ForeignKeyReferences;
|
|
66
|
-
readonly name?: string;
|
|
67
|
-
readonly onDelete?: ReferentialAction;
|
|
68
|
-
readonly onUpdate?: ReferentialAction;
|
|
69
|
-
/** Whether to emit FK constraint DDL (ALTER TABLE … ADD CONSTRAINT … FOREIGN KEY). */
|
|
70
|
-
readonly constraint: boolean;
|
|
71
|
-
/** Whether to emit a backing index for the FK columns. */
|
|
72
|
-
readonly index: boolean;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
export type StorageTable = {
|
|
76
|
-
readonly columns: Record<string, StorageColumn>;
|
|
77
|
-
readonly primaryKey?: PrimaryKey;
|
|
78
|
-
readonly uniques: ReadonlyArray<UniqueConstraint>;
|
|
79
|
-
readonly indexes: ReadonlyArray<Index>;
|
|
80
|
-
readonly foreignKeys: ReadonlyArray<ForeignKey>;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* A named, parameterized type instance.
|
|
85
|
-
* These are registered in `storage.types` for reuse across columns
|
|
86
|
-
* and to enable ergonomic schema surfaces like `schema.types.MyType`.
|
|
87
|
-
*
|
|
88
|
-
* Unlike {@link StorageColumn}, `typeParams` is required here because
|
|
89
|
-
* `StorageTypeInstance` exists specifically to define reusable parameterized types.
|
|
90
|
-
* A type instance without parameters would be redundant—columns can reference
|
|
91
|
-
* the codec directly via `codecId`.
|
|
92
|
-
*/
|
|
93
|
-
export type StorageTypeInstance = {
|
|
94
|
-
readonly codecId: string;
|
|
95
|
-
readonly nativeType: string;
|
|
96
|
-
readonly typeParams: Record<string, unknown>;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export type SqlStorage<THash extends string = string> = StorageBase<THash> & {
|
|
100
|
-
readonly tables: Record<string, StorageTable>;
|
|
101
|
-
/**
|
|
102
|
-
* Named type instances for parameterized/custom types.
|
|
103
|
-
* Columns can reference these via `typeRef`.
|
|
104
|
-
*/
|
|
105
|
-
readonly types?: Record<string, StorageTypeInstance>;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
47
|
export type SqlModelFieldStorage = {
|
|
109
48
|
readonly column: string;
|
|
110
49
|
readonly codecId?: string;
|
package/src/validators.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { ContractValidationError } from '@prisma-next/contract/contract-validation-error';
|
|
2
|
+
import type { Contract, ContractField, ContractModel } from '@prisma-next/contract/types';
|
|
3
|
+
import { validateContractDomain } from '@prisma-next/contract/validate-domain';
|
|
3
4
|
import { type } from 'arktype';
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
ReferentialAction,
|
|
5
|
+
import {
|
|
6
|
+
type ForeignKeyInput,
|
|
7
|
+
type ForeignKeyReferencesInput,
|
|
8
|
+
type PrimaryKeyInput,
|
|
9
|
+
type ReferentialAction,
|
|
10
|
+
type SqlModelStorage,
|
|
9
11
|
SqlStorage,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
type SqlStorageInput,
|
|
13
|
+
type StorageTypeInstanceInput,
|
|
14
|
+
type UniqueConstraintInput,
|
|
12
15
|
} from './types';
|
|
13
16
|
|
|
14
17
|
type ColumnDefaultLiteral = {
|
|
@@ -77,18 +80,66 @@ const StorageColumnSchema = type({
|
|
|
77
80
|
return true;
|
|
78
81
|
});
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Codec-triple entry persisted under `storage.types[name]`. Carries an
|
|
85
|
+
* enumerable literal `kind: 'codec-instance'` discriminator so the
|
|
86
|
+
* polymorphic slot dispatch can distinguish codec triples from
|
|
87
|
+
* class-instance kinds (e.g. `'postgres-enum'`) sharing the slot.
|
|
88
|
+
*/
|
|
89
|
+
const StorageTypeInstanceSchema = type
|
|
90
|
+
.declare<StorageTypeInstanceInput & { kind: 'codec-instance' }>()
|
|
91
|
+
.type({
|
|
92
|
+
kind: "'codec-instance'",
|
|
93
|
+
codecId: 'string',
|
|
94
|
+
nativeType: 'string',
|
|
95
|
+
typeParams: 'Record<string, unknown>',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Polymorphic enum-type entry under `storage.types[name]`. Carries an
|
|
100
|
+
* enumerable literal `kind: 'postgres-enum'` discriminator so the
|
|
101
|
+
* per-target hydration walker can dispatch cleanly back to a typed
|
|
102
|
+
* IR-class instance during `deserializeContract`. The discriminator
|
|
103
|
+
* reflects target-level behaviour (Postgres-native enums versus
|
|
104
|
+
* family-layer codec triples) — not the family abstract altitude alone.
|
|
105
|
+
*
|
|
106
|
+
* The schema literal lives at the family layer today because
|
|
107
|
+
* registry-driven validation for arbitrary slot shapes is not wired
|
|
108
|
+
* yet; once a second polymorphic kind ships through the slot, this
|
|
109
|
+
* structural enumeration can move to the registry-dispatch site and
|
|
110
|
+
* per-target schemas can live in their target packages.
|
|
111
|
+
*/
|
|
112
|
+
const PostgresEnumTypeSchema = type({
|
|
113
|
+
kind: "'postgres-enum'",
|
|
114
|
+
name: 'string',
|
|
82
115
|
nativeType: 'string',
|
|
83
|
-
|
|
116
|
+
values: type.string.array().readonly(),
|
|
84
117
|
});
|
|
85
118
|
|
|
86
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Family-layer arktype validation enumerates the polymorphic shapes the
|
|
121
|
+
* SQL family ships today (codec-instance + Postgres-enum). Pack-contributed
|
|
122
|
+
* entity types ship a parallel arktype schema entry here when they
|
|
123
|
+
* introduce a new persisted shape; the registry-driven hydration seam at
|
|
124
|
+
* `SqlContractSerializerBase.hydrateStorageTypeEntry` is open, but the
|
|
125
|
+
* family-layer structural validator is closed by design — extension
|
|
126
|
+
* packs cannot inject arbitrary persisted shapes through the slot
|
|
127
|
+
* without their structural shape being known at the family layer.
|
|
128
|
+
*
|
|
129
|
+
* A future refinement is to lift `StorageTypeEntrySchema` toward an
|
|
130
|
+
* `unknown` fallback and move structural diagnostics to the
|
|
131
|
+
* registry-dispatch site at hydration time, earned once a non-enum
|
|
132
|
+
* storage shape needs to flow through the slot without growing another
|
|
133
|
+
* closed union arm here first.
|
|
134
|
+
*/
|
|
135
|
+
const StorageTypeEntrySchema = PostgresEnumTypeSchema.or(StorageTypeInstanceSchema);
|
|
136
|
+
|
|
137
|
+
const PrimaryKeySchema = type.declare<PrimaryKeyInput>().type({
|
|
87
138
|
columns: type.string.array().readonly(),
|
|
88
139
|
'name?': 'string',
|
|
89
140
|
});
|
|
90
141
|
|
|
91
|
-
const UniqueConstraintSchema = type.declare<
|
|
142
|
+
const UniqueConstraintSchema = type.declare<UniqueConstraintInput>().type({
|
|
92
143
|
columns: type.string.array().readonly(),
|
|
93
144
|
'name?': 'string',
|
|
94
145
|
});
|
|
@@ -100,7 +151,7 @@ export const IndexSchema = type({
|
|
|
100
151
|
'options?': 'Record<string, unknown>',
|
|
101
152
|
});
|
|
102
153
|
|
|
103
|
-
export const ForeignKeyReferencesSchema = type.declare<
|
|
154
|
+
export const ForeignKeyReferencesSchema = type.declare<ForeignKeyReferencesInput>().type({
|
|
104
155
|
table: 'string',
|
|
105
156
|
columns: type.string.array().readonly(),
|
|
106
157
|
});
|
|
@@ -109,7 +160,7 @@ export const ReferentialActionSchema = type
|
|
|
109
160
|
.declare<ReferentialAction>()
|
|
110
161
|
.type("'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault'");
|
|
111
162
|
|
|
112
|
-
export const ForeignKeySchema = type.declare<
|
|
163
|
+
export const ForeignKeySchema = type.declare<ForeignKeyInput>().type({
|
|
113
164
|
columns: type.string.array().readonly(),
|
|
114
165
|
references: ForeignKeyReferencesSchema,
|
|
115
166
|
'name?': 'string',
|
|
@@ -128,11 +179,25 @@ const StorageTableSchema = type({
|
|
|
128
179
|
foreignKeys: ForeignKeySchema.array().readonly(),
|
|
129
180
|
});
|
|
130
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Namespace entry under `storage.namespaces[id]`. SQL contracts honour
|
|
184
|
+
* the framework `Storage.namespaces` invariant from PR1; today every
|
|
185
|
+
* contract binds to the singleton placeholder
|
|
186
|
+
* (`SqlUnspecifiedNamespace.instance`) and the persisted shape carries
|
|
187
|
+
* just the namespace id. Per-target namespace concretions
|
|
188
|
+
* (`PostgresSchema`, `SqliteUnspecifiedDatabase`) can additively grow
|
|
189
|
+
* the persisted shape when they earn their slots.
|
|
190
|
+
*/
|
|
191
|
+
const NamespaceEntrySchema = type({
|
|
192
|
+
id: 'string',
|
|
193
|
+
});
|
|
194
|
+
|
|
131
195
|
const StorageSchema = type({
|
|
132
196
|
'+': 'reject',
|
|
133
197
|
storageHash: 'string',
|
|
134
198
|
tables: type({ '[string]': StorageTableSchema }),
|
|
135
|
-
'types?': type({ '[string]':
|
|
199
|
+
'types?': type({ '[string]': StorageTypeEntrySchema }),
|
|
200
|
+
'namespaces?': type({ '[string]': NamespaceEntrySchema }),
|
|
136
201
|
});
|
|
137
202
|
|
|
138
203
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -228,7 +293,6 @@ const SqlContractSchema = type({
|
|
|
228
293
|
// includes bigint | Date (runtime-only types after decoding) which cannot be expressed
|
|
229
294
|
// in Arktype's JSON validation DSL. The `as SqlStorage` cast in validateStorage() bridges
|
|
230
295
|
// the gap between the JSON-safe Arktype output and the runtime TypeScript type.
|
|
231
|
-
// See decodeContractDefaults() in validate.ts for the decoding step.
|
|
232
296
|
|
|
233
297
|
/**
|
|
234
298
|
* Validates the structural shape of SqlStorage using Arktype.
|
|
@@ -243,7 +307,11 @@ export function validateStorage(value: unknown): SqlStorage {
|
|
|
243
307
|
const messages = result.map((p: { message: string }) => p.message).join('; ');
|
|
244
308
|
throw new Error(`Storage validation failed: ${messages}`);
|
|
245
309
|
}
|
|
246
|
-
|
|
310
|
+
// The arktype-validated shape matches `SqlStorageInput`
|
|
311
|
+
// structurally. Funnel through the constructor so nested IR fields
|
|
312
|
+
// (`tables`, `types`) are normalised into class instances and the
|
|
313
|
+
// branded `storageHash` is preserved on the returned `SqlStorage`.
|
|
314
|
+
return new SqlStorage(result as SqlStorageInput);
|
|
247
315
|
}
|
|
248
316
|
|
|
249
317
|
export function validateModel(value: unknown): unknown {
|
|
@@ -256,16 +324,12 @@ export function validateModel(value: unknown): unknown {
|
|
|
256
324
|
}
|
|
257
325
|
|
|
258
326
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* @param value - The contract value to validate (typically from a JSON import)
|
|
265
|
-
* @returns The validated contract if structure is valid
|
|
266
|
-
* @throws ContractValidationError if the contract structure is invalid
|
|
327
|
+
* Structural arktype validation of an SQL contract envelope. Internal
|
|
328
|
+
* helper for {@link validateSqlContractFully} — exposed only inside
|
|
329
|
+
* this module, since the family seam-of-record is the
|
|
330
|
+
* `SqlContractSerializerBase.deserializeContract` SPI.
|
|
267
331
|
*/
|
|
268
|
-
|
|
332
|
+
function validateSqlContractStructure<T extends Contract<SqlStorage>>(value: unknown): T {
|
|
269
333
|
if (typeof value !== 'object' || value === null) {
|
|
270
334
|
throw new ContractValidationError(
|
|
271
335
|
'Contract structural validation failed: value must be an object',
|
|
@@ -449,3 +513,179 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
|
|
|
449
513
|
|
|
450
514
|
return errors;
|
|
451
515
|
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* SQL storage logical-consistency checks: every model.storage.table
|
|
519
|
+
* resolves to a real table, every model.storage.fields[*].column
|
|
520
|
+
* resolves to a real column, and value-object fields land on JSON-native
|
|
521
|
+
* columns. Throws `ContractValidationError` on the first mismatch.
|
|
522
|
+
*/
|
|
523
|
+
export function validateModelStorageReferences(contract: Contract<SqlStorage>): void {
|
|
524
|
+
const models = contract.models as Record<string, ContractModel<SqlModelStorage>>;
|
|
525
|
+
for (const [modelName, model] of Object.entries(models)) {
|
|
526
|
+
const storageTable = model.storage.table;
|
|
527
|
+
|
|
528
|
+
const table = contract.storage.tables[storageTable] as
|
|
529
|
+
| (typeof contract.storage.tables)[string]
|
|
530
|
+
| undefined;
|
|
531
|
+
if (!table) {
|
|
532
|
+
throw new ContractValidationError(
|
|
533
|
+
`Model "${modelName}" references non-existent table "${storageTable}"`,
|
|
534
|
+
'storage',
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const columnNames = new Set(Object.keys(table.columns));
|
|
539
|
+
for (const [fieldName, field] of Object.entries(model.storage.fields)) {
|
|
540
|
+
if (!columnNames.has(field.column)) {
|
|
541
|
+
throw new ContractValidationError(
|
|
542
|
+
`Model "${modelName}" field "${fieldName}" references non-existent column "${field.column}" in table "${storageTable}"`,
|
|
543
|
+
'storage',
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const JSON_NATIVE_TYPES = new Set(['json', 'jsonb']);
|
|
549
|
+
for (const [fieldName, domainField] of Object.entries(model.fields ?? {})) {
|
|
550
|
+
const f = domainField as ContractField;
|
|
551
|
+
if (f.type?.kind !== 'valueObject') continue;
|
|
552
|
+
const storageField = model.storage.fields[fieldName];
|
|
553
|
+
if (!storageField) continue;
|
|
554
|
+
const column = table.columns[storageField.column];
|
|
555
|
+
if (!column) continue;
|
|
556
|
+
if (!JSON_NATIVE_TYPES.has(column.nativeType)) {
|
|
557
|
+
throw new ContractValidationError(
|
|
558
|
+
`Model "${modelName}" field "${fieldName}" is a value object but storage column "${storageField.column}" has nativeType "${column.nativeType}" (expected json or jsonb)`,
|
|
559
|
+
'storage',
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Cross-table consistency checks for SQL storage: primary key, unique,
|
|
568
|
+
* index, and foreign key column references resolve to real columns;
|
|
569
|
+
* NOT NULL columns don't carry a literal `null` default; FK column
|
|
570
|
+
* counts match their referenced columns. Throws on the first mismatch.
|
|
571
|
+
*/
|
|
572
|
+
export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): void {
|
|
573
|
+
const tableNames = new Set(Object.keys(contract.storage.tables));
|
|
574
|
+
|
|
575
|
+
for (const [tableName, table] of Object.entries(contract.storage.tables)) {
|
|
576
|
+
const columnNames = new Set(Object.keys(table.columns));
|
|
577
|
+
|
|
578
|
+
if (table.primaryKey) {
|
|
579
|
+
for (const colName of table.primaryKey.columns) {
|
|
580
|
+
if (!columnNames.has(colName)) {
|
|
581
|
+
throw new ContractValidationError(
|
|
582
|
+
`Table "${tableName}" primaryKey references non-existent column "${colName}"`,
|
|
583
|
+
'storage',
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
for (const unique of table.uniques) {
|
|
590
|
+
for (const colName of unique.columns) {
|
|
591
|
+
if (!columnNames.has(colName)) {
|
|
592
|
+
throw new ContractValidationError(
|
|
593
|
+
`Table "${tableName}" unique constraint references non-existent column "${colName}"`,
|
|
594
|
+
'storage',
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
for (const index of table.indexes) {
|
|
601
|
+
for (const colName of index.columns) {
|
|
602
|
+
if (!columnNames.has(colName)) {
|
|
603
|
+
throw new ContractValidationError(
|
|
604
|
+
`Table "${tableName}" index references non-existent column "${colName}"`,
|
|
605
|
+
'storage',
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
for (const [colName, column] of Object.entries(table.columns)) {
|
|
612
|
+
if (!column.nullable && column.default?.kind === 'literal' && column.default.value === null) {
|
|
613
|
+
throw new ContractValidationError(
|
|
614
|
+
`Table "${tableName}" column "${colName}" is NOT NULL but has a literal null default`,
|
|
615
|
+
'storage',
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
for (const fk of table.foreignKeys) {
|
|
621
|
+
for (const colName of fk.columns) {
|
|
622
|
+
if (!columnNames.has(colName)) {
|
|
623
|
+
throw new ContractValidationError(
|
|
624
|
+
`Table "${tableName}" foreignKey references non-existent column "${colName}"`,
|
|
625
|
+
'storage',
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (!tableNames.has(fk.references.table)) {
|
|
631
|
+
throw new ContractValidationError(
|
|
632
|
+
`Table "${tableName}" foreignKey references non-existent table "${fk.references.table}"`,
|
|
633
|
+
'storage',
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const referencedTable = contract.storage.tables[fk.references.table];
|
|
638
|
+
if (!referencedTable) continue;
|
|
639
|
+
const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
|
|
640
|
+
for (const colName of fk.references.columns) {
|
|
641
|
+
if (!referencedColumnNames.has(colName)) {
|
|
642
|
+
throw new ContractValidationError(
|
|
643
|
+
`Table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.references.table}"`,
|
|
644
|
+
'storage',
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (fk.columns.length !== fk.references.columns.length) {
|
|
650
|
+
throw new ContractValidationError(
|
|
651
|
+
`Table "${tableName}" foreignKey column count (${fk.columns.length}) does not match referenced column count (${fk.references.columns.length})`,
|
|
652
|
+
'storage',
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Full SQL contract validation: structural (arktype) +
|
|
661
|
+
* framework-shared domain + SQL storage logical-consistency + SQL
|
|
662
|
+
* storage semantic + model ↔ storage reference checks. Throws
|
|
663
|
+
* `ContractValidationError` on the first failure. Returns the
|
|
664
|
+
* validated flat-data shape; IR class hydration happens in the SPI
|
|
665
|
+
* base on top of this helper.
|
|
666
|
+
*/
|
|
667
|
+
export function validateSqlContractFully<T extends Contract<SqlStorage>>(value: unknown): T {
|
|
668
|
+
const stripped =
|
|
669
|
+
typeof value === 'object' && value !== null
|
|
670
|
+
? (() => {
|
|
671
|
+
const { schemaVersion: _, _generated: _g, ...rest } = value as Record<string, unknown>;
|
|
672
|
+
return rest;
|
|
673
|
+
})()
|
|
674
|
+
: value;
|
|
675
|
+
const validated = validateSqlContractStructure<T>(stripped);
|
|
676
|
+
validateContractDomain({
|
|
677
|
+
roots: validated.roots,
|
|
678
|
+
models: validated.models,
|
|
679
|
+
...(validated.valueObjects ? { valueObjects: validated.valueObjects } : {}),
|
|
680
|
+
});
|
|
681
|
+
validateSqlStorageConsistency(validated);
|
|
682
|
+
const semanticErrors = validateStorageSemantics(validated.storage);
|
|
683
|
+
if (semanticErrors.length > 0) {
|
|
684
|
+
throw new ContractValidationError(
|
|
685
|
+
`Contract semantic validation failed: ${semanticErrors.join('; ')}`,
|
|
686
|
+
'storage',
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
validateModelStorageReferences(validated);
|
|
690
|
+
return validated;
|
|
691
|
+
}
|
package/dist/types-hgzy8ME1.mjs
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
//#region src/types.ts
|
|
2
|
-
const DEFAULT_FK_CONSTRAINT = true;
|
|
3
|
-
const DEFAULT_FK_INDEX = true;
|
|
4
|
-
function applyFkDefaults(fk, overrideDefaults) {
|
|
5
|
-
return {
|
|
6
|
-
constraint: fk.constraint ?? overrideDefaults?.constraint ?? true,
|
|
7
|
-
index: fk.index ?? overrideDefaults?.index ?? true
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
//#endregion
|
|
11
|
-
export { DEFAULT_FK_INDEX as n, applyFkDefaults as r, DEFAULT_FK_CONSTRAINT as t };
|
|
12
|
-
|
|
13
|
-
//# sourceMappingURL=types-hgzy8ME1.mjs.map
|