@prisma-next/config 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/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@prisma-next/config",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Prisma Next config authoring types and validation",
7
+ "scripts": {
8
+ "build": "tsdown",
9
+ "test": "vitest run --passWithNoTests",
10
+ "test:coverage": "vitest run --coverage --passWithNoTests",
11
+ "typecheck": "tsc --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/contract": "workspace:*",
19
+ "@prisma-next/utils": "workspace:*",
20
+ "arktype": "^2.1.26"
21
+ },
22
+ "devDependencies": {
23
+ "@prisma-next/tsconfig": "workspace:*",
24
+ "@prisma-next/tsdown": "workspace:*",
25
+ "tsdown": "catalog:",
26
+ "typescript": "catalog:",
27
+ "vitest": "catalog:"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src"
32
+ ],
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "exports": {
37
+ "./config-types": "./dist/config-types.mjs",
38
+ "./config-validation": "./dist/config-validation.mjs",
39
+ "./types": "./dist/types.mjs",
40
+ "./package.json": "./package.json"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/prisma/prisma-next.git",
45
+ "directory": "packages/1-framework/1-core/shared/config"
46
+ }
47
+ }
@@ -0,0 +1,164 @@
1
+ import { type } from 'arktype';
2
+ import type { ContractSourceProvider } from './contract-source-types';
3
+ import type {
4
+ ControlAdapterDescriptor,
5
+ ControlDriverDescriptor,
6
+ ControlDriverInstance,
7
+ ControlExtensionDescriptor,
8
+ ControlFamilyDescriptor,
9
+ ControlTargetDescriptor,
10
+ } from './types';
11
+
12
+ /**
13
+ * Type alias for CLI driver instances.
14
+ * Uses string for both family and target IDs for maximum flexibility.
15
+ */
16
+ export type CliDriver = ControlDriverInstance<string, string>;
17
+
18
+ /**
19
+ * Contract configuration specifying source and artifact locations.
20
+ */
21
+ export interface ContractConfig {
22
+ /**
23
+ * Contract source provider. The provider is always async and must return
24
+ * a Result containing either ContractIR or structured diagnostics.
25
+ */
26
+ readonly source: ContractSourceProvider;
27
+ /**
28
+ * Path to contract.json artifact. Defaults to 'src/prisma/contract.json'.
29
+ * The .d.ts types file will be colocated (e.g., contract.json -> contract.d.ts).
30
+ */
31
+ readonly output?: string;
32
+ }
33
+
34
+ /**
35
+ * Configuration for Prisma Next CLI.
36
+ * Uses Control*Descriptor types for type-safe wiring with compile-time compatibility checks.
37
+ *
38
+ * @template TFamilyId - The family ID (e.g., 'sql', 'document')
39
+ * @template TTargetId - The target ID (e.g., 'postgres', 'mysql')
40
+ * @template TConnection - The driver connection input type (defaults to `unknown` for config flexibility)
41
+ */
42
+ export interface PrismaNextConfig<
43
+ TFamilyId extends string = string,
44
+ TTargetId extends string = string,
45
+ TConnection = unknown,
46
+ > {
47
+ readonly family: ControlFamilyDescriptor<TFamilyId>;
48
+ readonly target: ControlTargetDescriptor<TFamilyId, TTargetId>;
49
+ readonly adapter: ControlAdapterDescriptor<TFamilyId, TTargetId>;
50
+ readonly extensionPacks?: readonly ControlExtensionDescriptor<TFamilyId, TTargetId>[];
51
+ /**
52
+ * Driver descriptor for DB-connected CLI commands.
53
+ * Required for DB-connected commands (e.g., db verify).
54
+ * Optional for commands that don't need database access (e.g., emit).
55
+ * The driver's connection type matches the TConnection config parameter.
56
+ */
57
+ readonly driver?: ControlDriverDescriptor<
58
+ TFamilyId,
59
+ TTargetId,
60
+ ControlDriverInstance<TFamilyId, TTargetId>,
61
+ TConnection
62
+ >;
63
+ /**
64
+ * Database connection configuration.
65
+ * The connection type is driver-specific (e.g., URL string for Postgres).
66
+ */
67
+ readonly db?: {
68
+ /**
69
+ * Driver-specific connection input.
70
+ * For Postgres: a connection string (URL).
71
+ * For other drivers: may be a structured object.
72
+ */
73
+ readonly connection?: TConnection;
74
+ };
75
+ /**
76
+ * Contract configuration. Specifies source and artifact locations.
77
+ * Required for emit command; optional for other commands that only read artifacts.
78
+ */
79
+ readonly contract?: ContractConfig;
80
+ /**
81
+ * Migration configuration. Controls where on-disk migration packages are stored.
82
+ */
83
+ readonly migrations?: {
84
+ /** Directory for migration packages, relative to config file. Defaults to 'migrations'. */
85
+ readonly dir?: string;
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Arktype schema for ContractConfig validation.
91
+ * Validates presence/shape only.
92
+ * contract.source is validated as a provider function at runtime in defineConfig().
93
+ */
94
+ const ContractConfigSchema = type({
95
+ source: 'unknown', // Runtime check enforces provider function shape
96
+ 'output?': 'string',
97
+ });
98
+
99
+ /**
100
+ * Arktype schema for PrismaNextConfig validation.
101
+ * Note: This validates structure only. Descriptor objects (family, target, adapter) are validated separately.
102
+ */
103
+ const MigrationsConfigSchema = type({
104
+ 'dir?': 'string',
105
+ });
106
+
107
+ const PrismaNextConfigSchema = type({
108
+ family: 'unknown', // ControlFamilyDescriptor - validated separately
109
+ target: 'unknown', // ControlTargetDescriptor - validated separately
110
+ adapter: 'unknown', // ControlAdapterDescriptor - validated separately
111
+ 'extensionPacks?': 'unknown[]',
112
+ 'driver?': 'unknown', // ControlDriverDescriptor - validated separately (optional)
113
+ 'db?': 'unknown',
114
+ 'contract?': ContractConfigSchema,
115
+ 'migrations?': MigrationsConfigSchema,
116
+ });
117
+
118
+ /**
119
+ * Helper function to define a Prisma Next config.
120
+ * Validates and normalizes the config using Arktype, then returns the normalized IR.
121
+ *
122
+ * Normalization:
123
+ * - contract.output defaults to 'src/prisma/contract.json' if missing
124
+ *
125
+ * @param config - Raw config input from user
126
+ * @returns Normalized config IR with defaults applied
127
+ * @throws Error if config structure is invalid
128
+ */
129
+ export function defineConfig<TFamilyId extends string = string, TTargetId extends string = string>(
130
+ config: PrismaNextConfig<TFamilyId, TTargetId>,
131
+ ): PrismaNextConfig<TFamilyId, TTargetId> {
132
+ // Validate structure using Arktype
133
+ const validated = PrismaNextConfigSchema(config);
134
+ if (validated instanceof type.errors) {
135
+ const messages = validated.map((p: { message: string }) => p.message).join('; ');
136
+ throw new Error(`Config validation failed: ${messages}`);
137
+ }
138
+
139
+ // Normalize contract config if present
140
+ if (config.contract) {
141
+ // Validate contract.source provider function shape at runtime.
142
+ const source = config.contract.source;
143
+ if (typeof source !== 'function') {
144
+ throw new Error('Config.contract.source must be a provider function');
145
+ }
146
+
147
+ // Apply defaults
148
+ const output = config.contract.output ?? 'src/prisma/contract.json';
149
+
150
+ const normalizedContract: ContractConfig = {
151
+ source: config.contract.source,
152
+ output,
153
+ };
154
+
155
+ // Return normalized config
156
+ return {
157
+ ...config,
158
+ contract: normalizedContract,
159
+ };
160
+ }
161
+
162
+ // Return config as-is if no contract (preserve literal types)
163
+ return config;
164
+ }
@@ -0,0 +1,239 @@
1
+ import type { PrismaNextConfig } from './config-types';
2
+ import { ConfigValidationError } from './errors';
3
+
4
+ function throwValidation(field: string, why?: string): never {
5
+ throw new ConfigValidationError(field, why);
6
+ }
7
+
8
+ /**
9
+ * Validates that the config has the required structure.
10
+ * This is pure validation logic with no file I/O or CLI awareness.
11
+ *
12
+ * @param config - Config object to validate
13
+ * @throws ConfigValidationError if config structure is invalid
14
+ */
15
+ export function validateConfig(config: unknown): asserts config is PrismaNextConfig {
16
+ if (!config || typeof config !== 'object') {
17
+ throwValidation('object', 'Config must be an object');
18
+ }
19
+
20
+ const configObj = config as Record<string, unknown>;
21
+
22
+ if (!configObj['family']) {
23
+ throwValidation('family');
24
+ }
25
+
26
+ if (!configObj['target']) {
27
+ throwValidation('target');
28
+ }
29
+
30
+ if (!configObj['adapter']) {
31
+ throwValidation('adapter');
32
+ }
33
+
34
+ // Validate family descriptor
35
+ const family = configObj['family'] as Record<string, unknown>;
36
+ if (family['kind'] !== 'family') {
37
+ throwValidation('family.kind', 'Config.family must have kind: "family"');
38
+ }
39
+ if (typeof family['id'] !== 'string') {
40
+ throwValidation('family.id', 'Config.family must have id: string');
41
+ }
42
+ if (typeof family['familyId'] !== 'string') {
43
+ throwValidation('family.familyId', 'Config.family must have familyId: string');
44
+ }
45
+ if (typeof family['version'] !== 'string') {
46
+ throwValidation('family.version', 'Config.family must have version: string');
47
+ }
48
+ if (!family['hook'] || typeof family['hook'] !== 'object') {
49
+ throwValidation('family.hook', 'Config.family must have hook: TargetFamilyHook');
50
+ }
51
+ if (typeof family['create'] !== 'function') {
52
+ throwValidation('family.create', 'Config.family must have create: function');
53
+ }
54
+
55
+ const familyId = family['familyId'] as string;
56
+
57
+ // Validate target descriptor
58
+ const target = configObj['target'] as Record<string, unknown>;
59
+ if (target['kind'] !== 'target') {
60
+ throwValidation('target.kind', 'Config.target must have kind: "target"');
61
+ }
62
+ if (typeof target['id'] !== 'string') {
63
+ throwValidation('target.id', 'Config.target must have id: string');
64
+ }
65
+ if (typeof target['familyId'] !== 'string') {
66
+ throwValidation('target.familyId', 'Config.target must have familyId: string');
67
+ }
68
+ if (typeof target['version'] !== 'string') {
69
+ throwValidation('target.version', 'Config.target must have version: string');
70
+ }
71
+ if (target['familyId'] !== familyId) {
72
+ throwValidation(
73
+ 'target.familyId',
74
+ `Config.target.familyId must match Config.family.familyId (expected: ${familyId}, got: ${target['familyId']})`,
75
+ );
76
+ }
77
+ if (typeof target['targetId'] !== 'string') {
78
+ throwValidation('target.targetId', 'Config.target must have targetId: string');
79
+ }
80
+ if (typeof target['create'] !== 'function') {
81
+ throwValidation('target.create', 'Config.target must have create: function');
82
+ }
83
+ const expectedTargetId = target['targetId'] as string;
84
+
85
+ // Validate adapter descriptor
86
+ const adapter = configObj['adapter'] as Record<string, unknown>;
87
+ if (adapter['kind'] !== 'adapter') {
88
+ throwValidation('adapter.kind', 'Config.adapter must have kind: "adapter"');
89
+ }
90
+ if (typeof adapter['id'] !== 'string') {
91
+ throwValidation('adapter.id', 'Config.adapter must have id: string');
92
+ }
93
+ if (typeof adapter['familyId'] !== 'string') {
94
+ throwValidation('adapter.familyId', 'Config.adapter must have familyId: string');
95
+ }
96
+ if (typeof adapter['version'] !== 'string') {
97
+ throwValidation('adapter.version', 'Config.adapter must have version: string');
98
+ }
99
+ if (adapter['familyId'] !== familyId) {
100
+ throwValidation(
101
+ 'adapter.familyId',
102
+ `Config.adapter.familyId must match Config.family.familyId (expected: ${familyId}, got: ${adapter['familyId']})`,
103
+ );
104
+ }
105
+ if (typeof adapter['targetId'] !== 'string') {
106
+ throwValidation('adapter.targetId', 'Config.adapter must have targetId: string');
107
+ }
108
+ if (adapter['targetId'] !== expectedTargetId) {
109
+ throwValidation(
110
+ 'adapter.targetId',
111
+ `Config.adapter.targetId must match Config.target.targetId (expected: ${expectedTargetId}, got: ${adapter['targetId']})`,
112
+ );
113
+ }
114
+ if (typeof adapter['create'] !== 'function') {
115
+ throwValidation('adapter.create', 'Config.adapter must have create: function');
116
+ }
117
+
118
+ if (configObj['extensions'] !== undefined) {
119
+ throwValidation('extensions', 'Config.extensions is not supported; use Config.extensionPacks');
120
+ }
121
+
122
+ // Validate extensionPacks array if present
123
+ if (configObj['extensionPacks'] !== undefined) {
124
+ if (!Array.isArray(configObj['extensionPacks'])) {
125
+ throwValidation('extensionPacks', 'Config.extensionPacks must be an array');
126
+ }
127
+ for (const ext of configObj['extensionPacks']) {
128
+ if (!ext || typeof ext !== 'object') {
129
+ throwValidation(
130
+ 'extensionPacks[]',
131
+ 'Config.extensionPacks must contain ControlExtensionDescriptor objects',
132
+ );
133
+ }
134
+ const extObj = ext as Record<string, unknown>;
135
+ if (extObj['kind'] !== 'extension') {
136
+ throwValidation(
137
+ 'extensionPacks[].kind',
138
+ 'Config.extensionPacks items must have kind: "extension"',
139
+ );
140
+ }
141
+ if (typeof extObj['id'] !== 'string') {
142
+ throwValidation('extensionPacks[].id', 'Config.extensionPacks items must have id: string');
143
+ }
144
+ if (typeof extObj['familyId'] !== 'string') {
145
+ throwValidation(
146
+ 'extensionPacks[].familyId',
147
+ 'Config.extensionPacks items must have familyId: string',
148
+ );
149
+ }
150
+ if (typeof extObj['version'] !== 'string') {
151
+ throwValidation(
152
+ 'extensionPacks[].version',
153
+ 'Config.extensionPacks items must have version: string',
154
+ );
155
+ }
156
+ if (extObj['familyId'] !== familyId) {
157
+ throwValidation(
158
+ 'extensionPacks[].familyId',
159
+ `Config.extensionPacks[].familyId must match Config.family.familyId (expected: ${familyId}, got: ${extObj['familyId']})`,
160
+ );
161
+ }
162
+ if (typeof extObj['targetId'] !== 'string') {
163
+ throwValidation(
164
+ 'extensionPacks[].targetId',
165
+ 'Config.extensionPacks items must have targetId: string',
166
+ );
167
+ }
168
+ if (extObj['targetId'] !== expectedTargetId) {
169
+ throwValidation(
170
+ 'extensionPacks[].targetId',
171
+ `Config.extensionPacks[].targetId must match Config.target.targetId (expected: ${expectedTargetId}, got: ${extObj['targetId']})`,
172
+ );
173
+ }
174
+ if (typeof extObj['create'] !== 'function') {
175
+ throwValidation(
176
+ 'extensionPacks[].create',
177
+ 'Config.extensionPacks items must have create: function',
178
+ );
179
+ }
180
+ }
181
+ }
182
+
183
+ // Validate driver descriptor if present
184
+ if (configObj['driver'] !== undefined) {
185
+ const driver = configObj['driver'] as Record<string, unknown>;
186
+ if (driver['kind'] !== 'driver') {
187
+ throwValidation('driver.kind', 'Config.driver must have kind: "driver"');
188
+ }
189
+ if (typeof driver['id'] !== 'string') {
190
+ throwValidation('driver.id', 'Config.driver must have id: string');
191
+ }
192
+ if (typeof driver['version'] !== 'string') {
193
+ throwValidation('driver.version', 'Config.driver must have version: string');
194
+ }
195
+ if (typeof driver['familyId'] !== 'string') {
196
+ throwValidation('driver.familyId', 'Config.driver must have familyId: string');
197
+ }
198
+ if (driver['familyId'] !== familyId) {
199
+ throwValidation(
200
+ 'driver.familyId',
201
+ `Config.driver.familyId must match Config.family.familyId (expected: ${familyId}, got: ${driver['familyId']})`,
202
+ );
203
+ }
204
+ if (typeof driver['targetId'] !== 'string') {
205
+ throwValidation('driver.targetId', 'Config.driver must have targetId: string');
206
+ }
207
+ if (driver['targetId'] !== expectedTargetId) {
208
+ throwValidation(
209
+ 'driver.targetId',
210
+ `Config.driver.targetId must match Config.target.targetId (expected: ${expectedTargetId}, got: ${driver['targetId']})`,
211
+ );
212
+ }
213
+ if (typeof driver['create'] !== 'function') {
214
+ throwValidation('driver.create', 'Config.driver must have create: function');
215
+ }
216
+ }
217
+
218
+ // Validate contract config if present (structure validation - defineConfig() handles normalization)
219
+ if (configObj['contract'] !== undefined) {
220
+ const contract = configObj['contract'] as Record<string, unknown>;
221
+ if (!contract || typeof contract !== 'object') {
222
+ throwValidation('contract', 'Config.contract must be an object');
223
+ }
224
+ if (!Object.hasOwn(contract, 'source')) {
225
+ throwValidation(
226
+ 'contract.source',
227
+ 'Config.contract.source is required when contract is provided',
228
+ );
229
+ }
230
+
231
+ if (typeof contract['source'] !== 'function') {
232
+ throwValidation('contract.source', 'Config.contract.source must be a provider function');
233
+ }
234
+
235
+ if (contract['output'] !== undefined && typeof contract['output'] !== 'string') {
236
+ throwValidation('contract.output', 'Config.contract.output must be a string when provided');
237
+ }
238
+ }
239
+ }
@@ -0,0 +1,28 @@
1
+ import type { ContractIR } from '@prisma-next/contract/ir';
2
+ import type { Result } from '@prisma-next/utils/result';
3
+
4
+ export interface ContractSourceDiagnosticPosition {
5
+ readonly offset: number;
6
+ readonly line: number;
7
+ readonly column: number;
8
+ }
9
+
10
+ export interface ContractSourceDiagnosticSpan {
11
+ readonly start: ContractSourceDiagnosticPosition;
12
+ readonly end: ContractSourceDiagnosticPosition;
13
+ }
14
+
15
+ export interface ContractSourceDiagnostic {
16
+ readonly code: string;
17
+ readonly message: string;
18
+ readonly sourceId?: string;
19
+ readonly span?: ContractSourceDiagnosticSpan;
20
+ }
21
+
22
+ export interface ContractSourceDiagnostics {
23
+ readonly summary: string;
24
+ readonly diagnostics: readonly ContractSourceDiagnostic[];
25
+ readonly meta?: Record<string, unknown>;
26
+ }
27
+
28
+ export type ContractSourceProvider = () => Promise<Result<ContractIR, ContractSourceDiagnostics>>;
package/src/errors.ts ADDED
@@ -0,0 +1,11 @@
1
+ export class ConfigValidationError extends Error {
2
+ readonly field: string;
3
+ readonly why: string;
4
+
5
+ constructor(field: string, why?: string) {
6
+ super(why ?? `Config must have a "${field}" field`);
7
+ this.name = 'ConfigValidationError';
8
+ this.field = field;
9
+ this.why = why ?? `Config must have a "${field}" field`;
10
+ }
11
+ }
@@ -0,0 +1,9 @@
1
+ export type { ContractConfig, PrismaNextConfig } from '../config-types';
2
+ export { defineConfig } from '../config-types';
3
+ export type {
4
+ ContractSourceDiagnostic,
5
+ ContractSourceDiagnosticPosition,
6
+ ContractSourceDiagnosticSpan,
7
+ ContractSourceDiagnostics,
8
+ ContractSourceProvider,
9
+ } from '../contract-source-types';
@@ -0,0 +1,2 @@
1
+ export { validateConfig } from '../config-validation';
2
+ export { ConfigValidationError } from '../errors';
@@ -0,0 +1,13 @@
1
+ export type {
2
+ ControlAdapterDescriptor,
3
+ ControlAdapterInstance,
4
+ ControlDriverDescriptor,
5
+ ControlDriverInstance,
6
+ ControlExtensionDescriptor,
7
+ ControlExtensionInstance,
8
+ ControlFamilyDescriptor,
9
+ ControlFamilyInstance,
10
+ ControlPlaneStack,
11
+ ControlTargetDescriptor,
12
+ ControlTargetInstance,
13
+ } from '../types';