@zmdb/compiler 1.0.0-beta.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/LICENSE +674 -0
- package/README.md +40 -0
- package/dist/codegen/index.d.ts +50 -0
- package/dist/codegen/index.d.ts.map +1 -0
- package/dist/codegen/index.js +364 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/codegen/scan.d.ts +105 -0
- package/dist/codegen/scan.d.ts.map +1 -0
- package/dist/codegen/scan.js +450 -0
- package/dist/codegen/scan.js.map +1 -0
- package/dist/codegen/witness.d.ts +76 -0
- package/dist/codegen/witness.d.ts.map +1 -0
- package/dist/codegen/witness.js +690 -0
- package/dist/codegen/witness.js.map +1 -0
- package/dist/config/contract.d.ts +41 -0
- package/dist/config/contract.d.ts.map +1 -0
- package/dist/config/contract.js +5 -0
- package/dist/config/contract.js.map +1 -0
- package/dist/config/index.d.ts +53 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +277 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.zmdb.generated.d.ts +12 -0
- package/dist/config/index.zmdb.generated.js +28 -0
- package/dist/config/index.zmdb.witness.d.ts +3 -0
- package/dist/config/index.zmdb.witness.d.ts.map +1 -0
- package/dist/config/index.zmdb.witness.js +20 -0
- package/dist/config/index.zmdb.witness.js.map +1 -0
- package/dist/configured-plugin.d.ts +11 -0
- package/dist/configured-plugin.d.ts.map +1 -0
- package/dist/configured-plugin.js +27 -0
- package/dist/configured-plugin.js.map +1 -0
- package/dist/emit/index.d.ts +103 -0
- package/dist/emit/index.d.ts.map +1 -0
- package/dist/emit/index.js +1356 -0
- package/dist/emit/index.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/dist/lint/ast.d.ts +17 -0
- package/dist/lint/ast.d.ts.map +1 -0
- package/dist/lint/ast.js +77 -0
- package/dist/lint/ast.js.map +1 -0
- package/dist/lint/host-types.d.ts +9 -0
- package/dist/lint/host-types.d.ts.map +1 -0
- package/dist/lint/host-types.js +2 -0
- package/dist/lint/host-types.js.map +1 -0
- package/dist/lint/index.d.ts +23 -0
- package/dist/lint/index.d.ts.map +1 -0
- package/dist/lint/index.js +53 -0
- package/dist/lint/index.js.map +1 -0
- package/dist/lint/rules/no-distributed-nullable-tags.d.ts +3 -0
- package/dist/lint/rules/no-distributed-nullable-tags.d.ts.map +1 -0
- package/dist/lint/rules/no-distributed-nullable-tags.js +71 -0
- package/dist/lint/rules/no-distributed-nullable-tags.js.map +1 -0
- package/dist/lint/rules/no-empty-patch.d.ts +3 -0
- package/dist/lint/rules/no-empty-patch.d.ts.map +1 -0
- package/dist/lint/rules/no-empty-patch.js +26 -0
- package/dist/lint/rules/no-empty-patch.js.map +1 -0
- package/dist/lint/rules/no-interpolated-sql.d.ts +3 -0
- package/dist/lint/rules/no-interpolated-sql.d.ts.map +1 -0
- package/dist/lint/rules/no-interpolated-sql.js +35 -0
- package/dist/lint/rules/no-interpolated-sql.js.map +1 -0
- package/dist/lint/rules/no-unbounded-find.d.ts +3 -0
- package/dist/lint/rules/no-unbounded-find.d.ts.map +1 -0
- package/dist/lint/rules/no-unbounded-find.js +26 -0
- package/dist/lint/rules/no-unbounded-find.js.map +1 -0
- package/dist/lint/rules/no-unknown-json-column.d.ts +3 -0
- package/dist/lint/rules/no-unknown-json-column.d.ts.map +1 -0
- package/dist/lint/rules/no-unknown-json-column.js +35 -0
- package/dist/lint/rules/no-unknown-json-column.js.map +1 -0
- package/dist/lint/rules/require-sql-on-number.d.ts +3 -0
- package/dist/lint/rules/require-sql-on-number.d.ts.map +1 -0
- package/dist/lint/rules/require-sql-on-number.js +34 -0
- package/dist/lint/rules/require-sql-on-number.js.map +1 -0
- package/dist/lint/types.d.ts +12 -0
- package/dist/lint/types.d.ts.map +1 -0
- package/dist/lint/types.js +2 -0
- package/dist/lint/types.js.map +1 -0
- package/dist/metro/metro.d.ts +22 -0
- package/dist/metro/metro.d.ts.map +1 -0
- package/dist/metro/metro.js +242 -0
- package/dist/metro/metro.js.map +1 -0
- package/dist/protobuf/decode.d.ts +20 -0
- package/dist/protobuf/decode.d.ts.map +1 -0
- package/dist/protobuf/decode.js +289 -0
- package/dist/protobuf/decode.js.map +1 -0
- package/dist/protobuf/descriptor.d.ts +16 -0
- package/dist/protobuf/descriptor.d.ts.map +1 -0
- package/dist/protobuf/descriptor.js +303 -0
- package/dist/protobuf/descriptor.js.map +1 -0
- package/dist/protobuf/encode.d.ts +20 -0
- package/dist/protobuf/encode.d.ts.map +1 -0
- package/dist/protobuf/encode.js +203 -0
- package/dist/protobuf/encode.js.map +1 -0
- package/dist/protobuf/grpc-ir.d.ts +16 -0
- package/dist/protobuf/grpc-ir.d.ts.map +1 -0
- package/dist/protobuf/grpc-ir.js +2 -0
- package/dist/protobuf/grpc-ir.js.map +1 -0
- package/dist/protobuf/plan.d.ts +42 -0
- package/dist/protobuf/plan.d.ts.map +1 -0
- package/dist/protobuf/plan.js +158 -0
- package/dist/protobuf/plan.js.map +1 -0
- package/dist/reflect/callsites.d.ts +40 -0
- package/dist/reflect/callsites.d.ts.map +1 -0
- package/dist/reflect/callsites.js +153 -0
- package/dist/reflect/callsites.js.map +1 -0
- package/dist/reflect/index.d.ts +106 -0
- package/dist/reflect/index.d.ts.map +1 -0
- package/dist/reflect/index.js +1421 -0
- package/dist/reflect/index.js.map +1 -0
- package/dist/reflect/session.d.ts +118 -0
- package/dist/reflect/session.d.ts.map +1 -0
- package/dist/reflect/session.js +228 -0
- package/dist/reflect/session.js.map +1 -0
- package/dist/testing/index.d.ts +69 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +210 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/transform/index.d.ts +74 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +572 -0
- package/dist/transform/index.js.map +1 -0
- package/dist/unplugin/index.d.ts +53 -0
- package/dist/unplugin/index.d.ts.map +1 -0
- package/dist/unplugin/index.js +130 -0
- package/dist/unplugin/index.js.map +1 -0
- package/dist/unplugin/inline-bench.d.ts +29 -0
- package/dist/unplugin/inline-bench.d.ts.map +1 -0
- package/dist/unplugin/inline-bench.js +82 -0
- package/dist/unplugin/inline-bench.js.map +1 -0
- package/package.json +110 -0
- package/src/codegen/index.ts +451 -0
- package/src/codegen/scan.ts +574 -0
- package/src/codegen/witness.ts +819 -0
- package/src/config/contract.ts +48 -0
- package/src/config/index.ts +384 -0
- package/src/config/index.zmdb.generated.d.ts +12 -0
- package/src/config/index.zmdb.generated.js +28 -0
- package/src/config/index.zmdb.witness.ts +22 -0
- package/src/configured-plugin.ts +35 -0
- package/src/emit/index.ts +1496 -0
- package/src/errors.ts +6 -0
- package/src/index.ts +336 -0
- package/src/lint/ast.ts +96 -0
- package/src/lint/host-types.ts +9 -0
- package/src/lint/index.ts +80 -0
- package/src/lint/rules/no-distributed-nullable-tags.ts +76 -0
- package/src/lint/rules/no-empty-patch.ts +26 -0
- package/src/lint/rules/no-interpolated-sql.ts +45 -0
- package/src/lint/rules/no-unbounded-find.ts +26 -0
- package/src/lint/rules/no-unknown-json-column.ts +36 -0
- package/src/lint/rules/require-sql-on-number.ts +34 -0
- package/src/lint/types.ts +11 -0
- package/src/metro/metro.ts +313 -0
- package/src/protobuf/decode.ts +375 -0
- package/src/protobuf/descriptor.ts +353 -0
- package/src/protobuf/encode.ts +277 -0
- package/src/protobuf/grpc-ir.ts +17 -0
- package/src/protobuf/plan.ts +244 -0
- package/src/reflect/callsites.ts +198 -0
- package/src/reflect/index.ts +1723 -0
- package/src/reflect/session.ts +276 -0
- package/src/testing/index.ts +298 -0
- package/src/transform/index.ts +690 -0
- package/src/unplugin/index.ts +182 -0
- package/src/unplugin/inline-bench.ts +92 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { NamingStrategy } from '@zmdb/schema/naming';
|
|
2
|
+
import type { CompiledQuery, IntrospectOptions, SqlDialect } from '@zmdb/sql';
|
|
3
|
+
|
|
4
|
+
export type { IntrospectOptions } from '@zmdb/sql';
|
|
5
|
+
export type { NamingStrategy } from '@zmdb/schema/naming';
|
|
6
|
+
|
|
7
|
+
export interface HttpGenerationConfig {
|
|
8
|
+
readonly contracts: string | readonly string[];
|
|
9
|
+
readonly openApi: {
|
|
10
|
+
readonly out: string;
|
|
11
|
+
};
|
|
12
|
+
readonly client: {
|
|
13
|
+
readonly out: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Structural database boundary shared by compiler tools without importing the ORM. */
|
|
18
|
+
export interface ToolingDriver {
|
|
19
|
+
readonly dialect: SqlDialect;
|
|
20
|
+
execute(query: CompiledQuery): Promise<readonly Record<string, unknown>[]>;
|
|
21
|
+
transaction?<T>(run: (driver: ToolingDriver) => Promise<T>): Promise<T>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The plain-data half of a config, validated by the generated AOT checker. */
|
|
25
|
+
export interface ZmdbConfigData {
|
|
26
|
+
readonly schema: string | readonly string[];
|
|
27
|
+
readonly project?: string;
|
|
28
|
+
readonly out?: string;
|
|
29
|
+
readonly naming?: 'snake_case' | 'snake_case_plural';
|
|
30
|
+
readonly migrations?: {
|
|
31
|
+
readonly table?: string;
|
|
32
|
+
readonly schema?: string;
|
|
33
|
+
};
|
|
34
|
+
readonly introspect?: IntrospectOptions;
|
|
35
|
+
readonly http?: HttpGenerationConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The complete author-facing config, including the two callable boundaries. */
|
|
39
|
+
export interface ZmdbConfig extends ZmdbConfigData {
|
|
40
|
+
readonly dialect: SqlDialect;
|
|
41
|
+
readonly driver?: () => ToolingDriver | Promise<ToolingDriver>;
|
|
42
|
+
readonly namingStrategy?: NamingStrategy;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Identity helper for inference and editor completion; loading owns validation. */
|
|
46
|
+
export function defineConfig<const T extends ZmdbConfig>(config: T): T {
|
|
47
|
+
return config;
|
|
48
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { glob, stat } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, join, normalize, resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { isRecord } from '@zmdb/schema';
|
|
6
|
+
import { resolveNaming, type NamingStrategy } from '@zmdb/schema/naming';
|
|
7
|
+
import { isSqlDialect } from '@zmdb/sql';
|
|
8
|
+
import { AssertError } from '@zmdb/validator/errors';
|
|
9
|
+
|
|
10
|
+
import { projectSourceFileNames } from '../reflect/index.js';
|
|
11
|
+
import type { HttpGenerationConfig, ZmdbConfig, ZmdbConfigData } from './contract.js';
|
|
12
|
+
import { zmdbAssertZmdbConfigData } from './index.zmdb.generated.js';
|
|
13
|
+
|
|
14
|
+
export { defineConfig } from './contract.js';
|
|
15
|
+
export type {
|
|
16
|
+
HttpGenerationConfig,
|
|
17
|
+
IntrospectOptions,
|
|
18
|
+
NamingStrategy,
|
|
19
|
+
ToolingDriver,
|
|
20
|
+
ZmdbConfig,
|
|
21
|
+
ZmdbConfigData,
|
|
22
|
+
} from './contract.js';
|
|
23
|
+
|
|
24
|
+
const CONFIG_NAMES = ['zmdb.config.ts', 'zmdb.config.mjs', 'zmdb.config.js'] as const;
|
|
25
|
+
const CONFIG_CACHE = new Map<string, Promise<ResolvedConfig>>();
|
|
26
|
+
|
|
27
|
+
export interface ResolvedHttpContractSource {
|
|
28
|
+
readonly file: string;
|
|
29
|
+
readonly exportName: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ResolvedHttpGenerationConfig {
|
|
33
|
+
readonly contracts: readonly ResolvedHttpContractSource[];
|
|
34
|
+
readonly openApiOut: string;
|
|
35
|
+
readonly clientOut: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The concrete paths every command receives after discovery and validation. */
|
|
39
|
+
export type ResolvedConfig = Omit<ZmdbConfig, 'http'> & {
|
|
40
|
+
readonly configPath: string;
|
|
41
|
+
readonly project: string;
|
|
42
|
+
readonly out: string;
|
|
43
|
+
readonly schemaFiles: readonly string[];
|
|
44
|
+
readonly outDir: string;
|
|
45
|
+
readonly http?: ResolvedHttpGenerationConfig;
|
|
46
|
+
/** The custom or named strategy, resolved once for every reflection route. */
|
|
47
|
+
readonly resolvedNaming: NamingStrategy;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export interface LoadConfigOptions {
|
|
51
|
+
/** Directory discovery starts from. Defaults to `process.cwd()`. */
|
|
52
|
+
readonly cwd?: string;
|
|
53
|
+
/** Explicit config path, resolved against `cwd`; equivalent to `--config`. */
|
|
54
|
+
readonly path?: string;
|
|
55
|
+
/** Return `undefined` when discovery finds no config. An explicit missing path still fails. */
|
|
56
|
+
readonly optional?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Discover, execute, validate and resolve one config.
|
|
61
|
+
*
|
|
62
|
+
* The cache is process-local and keyed by the selected absolute config path, so
|
|
63
|
+
* separate packages in a monorepo cannot leak resolved paths into one another.
|
|
64
|
+
*/
|
|
65
|
+
export function loadConfig(
|
|
66
|
+
options: LoadConfigOptions & { readonly optional: true },
|
|
67
|
+
): Promise<ResolvedConfig | undefined>;
|
|
68
|
+
export function loadConfig(options?: LoadConfigOptions & { readonly optional?: false }): Promise<ResolvedConfig>;
|
|
69
|
+
export async function loadConfig(options: LoadConfigOptions = {}): Promise<ResolvedConfig | undefined> {
|
|
70
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
71
|
+
const configPath = await discoverConfig(cwd, options.path, options.optional === true);
|
|
72
|
+
if (configPath === undefined) return undefined;
|
|
73
|
+
const cached = CONFIG_CACHE.get(configPath);
|
|
74
|
+
if (cached !== undefined) return cached;
|
|
75
|
+
|
|
76
|
+
const pending = loadAndResolve(configPath).catch((error: unknown) => {
|
|
77
|
+
CONFIG_CACHE.delete(configPath);
|
|
78
|
+
throw error;
|
|
79
|
+
});
|
|
80
|
+
CONFIG_CACHE.set(configPath, pending);
|
|
81
|
+
return pending;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve an already-loaded config against its file.
|
|
86
|
+
*
|
|
87
|
+
* Commands normally call `loadConfig`; this separate boundary lets build tools
|
|
88
|
+
* that already imported the module reuse exactly the same validation and path
|
|
89
|
+
* rules without inventing a second resolver.
|
|
90
|
+
*/
|
|
91
|
+
export async function resolveConfig(config: ZmdbConfig, configPath: string): Promise<ResolvedConfig> {
|
|
92
|
+
const absolutePath = resolve(configPath);
|
|
93
|
+
return resolveValidatedConfig(validateConfig(config, absolutePath), absolutePath);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function loadAndResolve(configPath: string): Promise<ResolvedConfig> {
|
|
97
|
+
const loaded = await importConfig(configPath);
|
|
98
|
+
const config = validateConfig(loaded, configPath);
|
|
99
|
+
return resolveValidatedConfig(config, configPath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function discoverConfig(
|
|
103
|
+
cwd: string,
|
|
104
|
+
explicit: string | undefined,
|
|
105
|
+
optional: boolean,
|
|
106
|
+
): Promise<string | undefined> {
|
|
107
|
+
if (explicit !== undefined) return resolve(cwd, explicit);
|
|
108
|
+
|
|
109
|
+
let directory = cwd;
|
|
110
|
+
while (true) {
|
|
111
|
+
for (const name of CONFIG_NAMES) {
|
|
112
|
+
const candidate = join(directory, name);
|
|
113
|
+
if (await isFile(candidate)) return candidate;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (await isFile(join(directory, 'package.json'))) {
|
|
117
|
+
if (optional) return undefined;
|
|
118
|
+
throw new Error(`No zmdb config found from ${cwd}; discovery stopped at package boundary ${directory}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const parent = dirname(directory);
|
|
122
|
+
if (parent === directory) {
|
|
123
|
+
if (optional) return undefined;
|
|
124
|
+
throw new Error(`No zmdb config found from ${cwd}; discovery reached filesystem root ${directory}`);
|
|
125
|
+
}
|
|
126
|
+
directory = parent;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function importConfig(configPath: string): Promise<unknown> {
|
|
131
|
+
try {
|
|
132
|
+
const namespace = Object.fromEntries(Object.entries(await import(pathToFileURL(configPath).href)));
|
|
133
|
+
const names = Object.keys(namespace);
|
|
134
|
+
const hasDefault = Object.hasOwn(namespace, 'default');
|
|
135
|
+
const hasNamed = Object.hasOwn(namespace, 'config');
|
|
136
|
+
|
|
137
|
+
if (names.length !== 1 || hasDefault === hasNamed) {
|
|
138
|
+
const found = names.length === 0 ? 'no exports' : names.join(', ');
|
|
139
|
+
throw new Error(`expected exactly one default export or named \`config\` export; found ${found}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return await Promise.resolve(hasDefault ? namespace.default : namespace.config);
|
|
143
|
+
} catch (error: unknown) {
|
|
144
|
+
const hint =
|
|
145
|
+
errorCode(error) === 'ERR_MODULE_NOT_FOUND'
|
|
146
|
+
? ' Node does not remap a .js import specifier to a .ts source; name the real file or provide a Node loader hook.'
|
|
147
|
+
: '';
|
|
148
|
+
throw new Error(`Failed to load ${configPath}: ${errorMessages(error).join(': ')}.${hint}`, { cause: error });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateConfig(input: unknown, configPath: string): ZmdbConfig {
|
|
153
|
+
if (!isRecord(input)) {
|
|
154
|
+
throw new Error(`Invalid config ${configPath}: expected an object`, { cause: input });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const { dialect, driver, namingStrategy, ...dataInput } = input;
|
|
158
|
+
let data: ZmdbConfigData;
|
|
159
|
+
try {
|
|
160
|
+
data = zmdbAssertZmdbConfigData(dataInput);
|
|
161
|
+
} catch (error: unknown) {
|
|
162
|
+
if (error instanceof AssertError) {
|
|
163
|
+
const issue = error.issues[0];
|
|
164
|
+
const field = issue?.path.replace(/^input\.?/, '') || 'config';
|
|
165
|
+
throw new Error(`Invalid config ${configPath}: ${field} ${issue?.message ?? error.message}`, { cause: error });
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`Invalid config ${configPath}: ${errorMessages(error).join(': ')}`, { cause: error });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (driver !== undefined && !isDriverFactory(driver)) {
|
|
171
|
+
throw new Error(`Invalid config ${configPath}: driver must be a function`);
|
|
172
|
+
}
|
|
173
|
+
if (!isSqlDialect(dialect)) {
|
|
174
|
+
throw new Error(`Invalid config ${configPath}: dialect must be an imported SqlDialect object`);
|
|
175
|
+
}
|
|
176
|
+
if (namingStrategy !== undefined && !isNamingStrategy(namingStrategy)) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Invalid config ${configPath}: namingStrategy must be an object whose column, table and index members are functions`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
...data,
|
|
184
|
+
dialect,
|
|
185
|
+
...(driver === undefined ? {} : { driver }),
|
|
186
|
+
...(namingStrategy === undefined ? {} : { namingStrategy }),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function resolveValidatedConfig(config: ZmdbConfig, configPath: string): Promise<ResolvedConfig> {
|
|
191
|
+
const { http: configuredHttp, ...configWithoutHttp } = config;
|
|
192
|
+
const directory = dirname(configPath);
|
|
193
|
+
const project = resolve(directory, config.project ?? 'tsconfig.json');
|
|
194
|
+
const outDir = resolve(directory, config.out ?? 'migrations');
|
|
195
|
+
const resolvedNaming = resolveNaming(config.namingStrategy ?? config.naming);
|
|
196
|
+
|
|
197
|
+
if (
|
|
198
|
+
config.migrations?.schema !== undefined &&
|
|
199
|
+
config.dialect.family !== 'mssql' &&
|
|
200
|
+
config.dialect.family !== 'postgres'
|
|
201
|
+
) {
|
|
202
|
+
throw new Error(`Invalid config ${configPath}: migrations.schema is not supported by ${config.dialect.name}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let projectFiles: readonly string[];
|
|
206
|
+
try {
|
|
207
|
+
projectFiles = projectSourceFileNames(project);
|
|
208
|
+
} catch (error: unknown) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Failed to read TypeScript project ${project} for ${configPath}: ${errorMessages(error).join(': ')}`,
|
|
211
|
+
{
|
|
212
|
+
cause: error,
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const schemaFiles = await expandSchemaFiles(config.schema, directory, project, projectFiles, configPath);
|
|
218
|
+
const http =
|
|
219
|
+
configuredHttp === undefined
|
|
220
|
+
? undefined
|
|
221
|
+
: await resolveHttpGenerationConfig(configuredHttp, directory, project, projectFiles, configPath);
|
|
222
|
+
return {
|
|
223
|
+
...configWithoutHttp,
|
|
224
|
+
project,
|
|
225
|
+
out: outDir,
|
|
226
|
+
configPath,
|
|
227
|
+
schemaFiles,
|
|
228
|
+
outDir,
|
|
229
|
+
...(http === undefined ? {} : { http }),
|
|
230
|
+
resolvedNaming,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function resolveHttpGenerationConfig(
|
|
235
|
+
configured: HttpGenerationConfig,
|
|
236
|
+
directory: string,
|
|
237
|
+
project: string,
|
|
238
|
+
projectFiles: readonly string[],
|
|
239
|
+
configPath: string,
|
|
240
|
+
): Promise<ResolvedHttpGenerationConfig> {
|
|
241
|
+
const specifications = typeof configured.contracts === 'string' ? [configured.contracts] : configured.contracts;
|
|
242
|
+
if (specifications.length === 0) {
|
|
243
|
+
throw new Error(`Invalid config ${configPath}: http.contracts must contain at least one path#export`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const included = new Set(projectFiles.map(pathKey));
|
|
247
|
+
const seen = new Set<string>();
|
|
248
|
+
const contracts: ResolvedHttpContractSource[] = [];
|
|
249
|
+
for (const specification of specifications) {
|
|
250
|
+
const hash = specification.lastIndexOf('#');
|
|
251
|
+
if (hash <= 0 || hash === specification.length - 1) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Invalid config ${configPath}: HTTP contract ${JSON.stringify(specification)} must be <path>#<export>`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const namedPath = specification.slice(0, hash);
|
|
257
|
+
const exportName = specification.slice(hash + 1);
|
|
258
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(exportName)) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`Invalid config ${configPath}: HTTP contract export ${JSON.stringify(exportName)} is not an identifier`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const file = normalize(resolve(directory, namedPath));
|
|
265
|
+
if (!(await isFile(file))) {
|
|
266
|
+
throw new Error(`Invalid config ${configPath}: HTTP contract file ${file} does not exist`);
|
|
267
|
+
}
|
|
268
|
+
if (!included.has(pathKey(file))) {
|
|
269
|
+
throw new Error(`HTTP contract file ${file} is not included by ${project}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const key = `${pathKey(file)}\u0000${exportName}`;
|
|
273
|
+
if (seen.has(key)) {
|
|
274
|
+
throw new Error(`Invalid config ${configPath}: HTTP contract ${file}#${exportName} appears more than once`);
|
|
275
|
+
}
|
|
276
|
+
seen.add(key);
|
|
277
|
+
contracts.push({ file, exportName });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const openApiOut = normalize(resolve(directory, configured.openApi.out));
|
|
281
|
+
const clientOut = normalize(resolve(directory, configured.client.out));
|
|
282
|
+
if (!openApiOut.endsWith('.json')) {
|
|
283
|
+
throw new Error(`Invalid config ${configPath}: http.openApi.out must end in .json, received ${openApiOut}`);
|
|
284
|
+
}
|
|
285
|
+
if (!clientOut.endsWith('.ts')) {
|
|
286
|
+
throw new Error(`Invalid config ${configPath}: http.client.out must end in .ts, received ${clientOut}`);
|
|
287
|
+
}
|
|
288
|
+
if (pathKey(openApiOut) === pathKey(clientOut)) {
|
|
289
|
+
throw new Error(`Invalid config ${configPath}: http.openApi.out and http.client.out must be different files`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { contracts, openApiOut, clientOut };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function expandSchemaFiles(
|
|
296
|
+
configured: string | readonly string[],
|
|
297
|
+
directory: string,
|
|
298
|
+
project: string,
|
|
299
|
+
projectFiles: readonly string[],
|
|
300
|
+
configPath: string,
|
|
301
|
+
): Promise<readonly string[]> {
|
|
302
|
+
const patterns = typeof configured === 'string' ? [configured] : configured;
|
|
303
|
+
if (patterns.length === 0) throw new Error(`Invalid config ${configPath}: schema must contain at least one glob`);
|
|
304
|
+
|
|
305
|
+
const included = new Set(projectFiles.map(pathKey));
|
|
306
|
+
const selected = new Set<string>();
|
|
307
|
+
|
|
308
|
+
for (const pattern of patterns) {
|
|
309
|
+
if (pattern.length === 0) throw new Error(`Invalid config ${configPath}: schema contains an empty glob`);
|
|
310
|
+
const matches: string[] = [];
|
|
311
|
+
try {
|
|
312
|
+
for await (const match of glob(pattern, { cwd: directory })) {
|
|
313
|
+
const absolute = resolveGlobMatch(match, directory);
|
|
314
|
+
if (await isFile(absolute)) matches.push(absolute);
|
|
315
|
+
}
|
|
316
|
+
} catch (error: unknown) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
`Invalid schema glob ${JSON.stringify(pattern)} in ${configPath}: ${errorMessages(error).join(': ')}`,
|
|
319
|
+
{
|
|
320
|
+
cause: error,
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (matches.length === 0) {
|
|
326
|
+
throw new Error(`Schema glob ${JSON.stringify(pattern)} in ${configPath} matched no files`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (const file of matches) {
|
|
330
|
+
if (!included.has(pathKey(file))) {
|
|
331
|
+
throw new Error(`Schema file ${file} matched ${JSON.stringify(pattern)} but is not included by ${project}`);
|
|
332
|
+
}
|
|
333
|
+
selected.add(file);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return [...selected].toSorted();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function resolveGlobMatch(match: string, directory: string): string {
|
|
341
|
+
return normalize(isAbsolute(match) ? match : resolve(directory, match));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function pathKey(path: string): string {
|
|
345
|
+
const normalized = normalize(path);
|
|
346
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function isFile(path: string): Promise<boolean> {
|
|
350
|
+
try {
|
|
351
|
+
return (await stat(path)).isFile();
|
|
352
|
+
} catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function isDriverFactory(value: unknown): value is NonNullable<ZmdbConfig['driver']> {
|
|
358
|
+
return typeof value === 'function';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function isNamingStrategy(value: unknown): value is NamingStrategy {
|
|
362
|
+
if (!isRecord(value)) return false;
|
|
363
|
+
for (const key of ['column', 'table', 'index'] as const) {
|
|
364
|
+
const member = value[key];
|
|
365
|
+
if (member !== undefined && typeof member !== 'function') return false;
|
|
366
|
+
}
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function errorCode(error: unknown): string | undefined {
|
|
371
|
+
if (!isRecord(error)) return undefined;
|
|
372
|
+
return typeof error.code === 'string' ? error.code : undefined;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function errorMessages(error: unknown, seen = new Set<unknown>()): readonly string[] {
|
|
376
|
+
if (seen.has(error)) return [];
|
|
377
|
+
seen.add(error);
|
|
378
|
+
|
|
379
|
+
if (error instanceof Error) {
|
|
380
|
+
const nested = error.cause === undefined ? [] : errorMessages(error.cause, seen);
|
|
381
|
+
return [error.message, ...nested];
|
|
382
|
+
}
|
|
383
|
+
return [String(error)];
|
|
384
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Generated by @zmdb/compiler from src/config/index.ts. Do not edit.
|
|
2
|
+
//
|
|
3
|
+
// The validators, compiled. Every check below was emitted from a type in src/config/index.ts's
|
|
4
|
+
// witness module by `@zmdb/compiler`, so there is no schema to walk at runtime and
|
|
5
|
+
// nothing to look up: the shape is the control flow.
|
|
6
|
+
//
|
|
7
|
+
// This half is the types: the signatures the compiler reads when the source imports the
|
|
8
|
+
// module next door. Nothing here is checked against the implementation, and nothing needs
|
|
9
|
+
// to be — the witness makes the same claims against the runtime API, and *that* is checked.
|
|
10
|
+
import type { ZmdbConfigData } from './contract.js';
|
|
11
|
+
|
|
12
|
+
export declare function zmdbAssertZmdbConfigData(value: unknown): ZmdbConfigData;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Generated by @zmdb/compiler from src/config/index.ts. Do not edit.
|
|
2
|
+
//
|
|
3
|
+
// The validators, compiled. Every check below was emitted from a type in src/config/index.ts's
|
|
4
|
+
// witness module by `@zmdb/compiler`, so there is no schema to walk at runtime and
|
|
5
|
+
// nothing to look up: the shape is the control flow.
|
|
6
|
+
//
|
|
7
|
+
// JavaScript rather than TypeScript on purpose — the emitted helpers are untyped, and a
|
|
8
|
+
// generated file that needed `any` to typecheck would be a worse trade than one that is
|
|
9
|
+
// simply not typechecked. The declarations live in the sidecar `.d.ts`.
|
|
10
|
+
//
|
|
11
|
+
// Formatters and linters should skip it. The emitter writes a check as one long expression
|
|
12
|
+
// because that is what it is; reformatting would leave the next compiler check
|
|
13
|
+
// permanently out of date against a file nothing was wrong with.
|
|
14
|
+
import { AssertError as _zmdbAssertError } from "@zmdb/validator";
|
|
15
|
+
function _zmdbCheckZmdbConfigData0(_v) { return typeof _v === "object" && _v !== null && !Array.isArray(_v) && (((typeof _v.schema === "string") || (_zmdbCheckArray1(_v.schema)))) && (_v.project === undefined || (typeof _v.project === "string")) && (_v.out === undefined || (typeof _v.out === "string")) && (_v.naming === undefined || (((_v.naming === "snake_case") || (_v.naming === "snake_case_plural")))) && (_v.migrations === undefined || (typeof _v.migrations === "object" && _v.migrations !== null && !Array.isArray(_v.migrations) && (_v.migrations.table === undefined || (typeof _v.migrations.table === "string")) && (_v.migrations.schema === undefined || (typeof _v.migrations.schema === "string")))) && (_v.introspect === undefined || (_zmdbCheckIntrospectOptions2(_v.introspect))) && (_v.http === undefined || (_zmdbCheckHttpGenerationConfig3(_v.http))); }
|
|
16
|
+
function _zmdbCheckArray1(_v) { if (!Array.isArray(_v)) return false; for (let _i = 0; _i < _v.length; _i++) { if (!(typeof _v[_i] === "string")) return false; } return true; }
|
|
17
|
+
function _zmdbCheckIntrospectOptions2(_v) { return typeof _v === "object" && _v !== null && !Array.isArray(_v) && (_v.schemas === undefined || (_zmdbCheckArray1(_v.schemas))) && (_v.include === undefined || (_zmdbCheckArray1(_v.include))) && (_v.exclude === undefined || (_zmdbCheckArray1(_v.exclude))); }
|
|
18
|
+
function _zmdbCheckHttpGenerationConfig3(_v) { return typeof _v === "object" && _v !== null && !Array.isArray(_v) && (((typeof _v.contracts === "string") || (_zmdbCheckArray1(_v.contracts)))) && (typeof _v.openApi === "object" && _v.openApi !== null && !Array.isArray(_v.openApi) && (typeof _v.openApi.out === "string")) && (typeof _v.client === "object" && _v.client !== null && !Array.isArray(_v.client) && (typeof _v.client.out === "string")); }
|
|
19
|
+
function _zmdbIssues4(_v, _p, _o) { _zmdbIssuesZmdbConfigData5(_v, _p, _o); }
|
|
20
|
+
function _zmdbIssuesZmdbConfigData5(_v, _p, _o) { if (!(typeof _v === "object" && _v !== null && !Array.isArray(_v))) { _zmdbIssue(_o, _p, "ZmdbConfigData", _v); } else { if (!(((typeof _v.schema === "string") || (_zmdbCheckArray1(_v.schema))))) _zmdbIssue(_o, _p + ".schema", "string | array", _v.schema); if (_v.project !== undefined) { if (!(typeof _v.project === "string")) _zmdbIssue(_o, _p + ".project", "string", _v.project); } if (_v.out !== undefined) { if (!(typeof _v.out === "string")) _zmdbIssue(_o, _p + ".out", "string", _v.out); } if (_v.naming !== undefined) { if (!(((_v.naming === "snake_case") || (_v.naming === "snake_case_plural")))) _zmdbIssue(_o, _p + ".naming", "\"snake_case\" | \"snake_case_plural\"", _v.naming); } if (_v.migrations !== undefined) { if (!(typeof _v.migrations === "object" && _v.migrations !== null && !Array.isArray(_v.migrations))) { _zmdbIssue(_o, _p + ".migrations", "object", _v.migrations); } else { if (_v.migrations.table !== undefined) { if (!(typeof _v.migrations.table === "string")) _zmdbIssue(_o, _p + ".migrations" + ".table", "string", _v.migrations.table); } if (_v.migrations.schema !== undefined) { if (!(typeof _v.migrations.schema === "string")) _zmdbIssue(_o, _p + ".migrations" + ".schema", "string", _v.migrations.schema); } } } if (_v.introspect !== undefined) { _zmdbIssuesIntrospectOptions6(_v.introspect, _p + ".introspect", _o); } if (_v.http !== undefined) { _zmdbIssuesHttpGenerationConfig8(_v.http, _p + ".http", _o); } } }
|
|
21
|
+
function _zmdbIssue(out, path, expected, value) { out.push({ path, expected, value, message: "expected " + expected }); }
|
|
22
|
+
function _zmdbIssuesIntrospectOptions6(_v, _p, _o) { if (!(typeof _v === "object" && _v !== null && !Array.isArray(_v))) { _zmdbIssue(_o, _p, "IntrospectOptions", _v); } else { if (_v.schemas !== undefined) { _zmdbIssuesArray7(_v.schemas, _p + ".schemas", _o); } if (_v.include !== undefined) { _zmdbIssuesArray7(_v.include, _p + ".include", _o); } if (_v.exclude !== undefined) { _zmdbIssuesArray7(_v.exclude, _p + ".exclude", _o); } } }
|
|
23
|
+
function _zmdbIssuesArray7(_v, _p, _o) { if (!Array.isArray(_v)) { _zmdbIssue(_o, _p, "array", _v); return; } for (let _i = 0; _i < _v.length; _i++) { if (!(typeof _v[_i] === "string")) _zmdbIssue(_o, _p + "[" + _i + "]", "string", _v[_i]); } }
|
|
24
|
+
function _zmdbIssuesHttpGenerationConfig8(_v, _p, _o) { if (!(typeof _v === "object" && _v !== null && !Array.isArray(_v))) { _zmdbIssue(_o, _p, "HttpGenerationConfig", _v); } else { if (!(((typeof _v.contracts === "string") || (_zmdbCheckArray1(_v.contracts))))) _zmdbIssue(_o, _p + ".contracts", "string | array", _v.contracts); if (!(typeof _v.openApi === "object" && _v.openApi !== null && !Array.isArray(_v.openApi))) { _zmdbIssue(_o, _p + ".openApi", "object", _v.openApi); } else { if (!(typeof _v.openApi.out === "string")) _zmdbIssue(_o, _p + ".openApi" + ".out", "string", _v.openApi.out); } if (!(typeof _v.client === "object" && _v.client !== null && !Array.isArray(_v.client))) { _zmdbIssue(_o, _p + ".client", "object", _v.client); } else { if (!(typeof _v.client.out === "string")) _zmdbIssue(_o, _p + ".client" + ".out", "string", _v.client.out); } } }
|
|
25
|
+
|
|
26
|
+
export function zmdbAssertZmdbConfigData(value) {
|
|
27
|
+
return ((() => { if ((_zmdbCheckZmdbConfigData0(value))) return value; const _e = []; _zmdbIssues4(value, "input", _e); throw new _zmdbAssertError(_e[0] ? _e[0].message : "validation failed", _e); })());
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*zmdb:imports*/
|
|
2
|
+
// Generated by @zmdb/compiler from src/config/index.ts. Checked, not edited.
|
|
3
|
+
//
|
|
4
|
+
// One wrapper per validation call src/config/index.ts used to make, written against the runtime
|
|
5
|
+
// API so that the compiler still sees each type argument. That is the point of the file: it
|
|
6
|
+
// is what the project compiler reads on the next run to regenerate the compiled module, and it is
|
|
7
|
+
// what turns a renamed or deleted type into a build error here instead of a validator that
|
|
8
|
+
// silently checks a shape nobody declares.
|
|
9
|
+
//
|
|
10
|
+
// Editing it by hand does nothing. An entry disappears when the source stops referring to
|
|
11
|
+
// its export name, so deleting a call in the source is how you delete a validator; there's
|
|
12
|
+
// no bookkeeping to keep in step.
|
|
13
|
+
|
|
14
|
+
import type { ZmdbConfigData } from './contract.js';
|
|
15
|
+
import { assert } from '@zmdb/validator';
|
|
16
|
+
/*zmdb:/imports*/
|
|
17
|
+
|
|
18
|
+
/*zmdb:begin:zmdbAssertZmdbConfigData*/
|
|
19
|
+
export function zmdbAssertZmdbConfigData(value: unknown): ZmdbConfigData {
|
|
20
|
+
return assert<ZmdbConfigData>(value);
|
|
21
|
+
}
|
|
22
|
+
/*zmdb:end:zmdbAssertZmdbConfigData*/
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { loadConfig } from './config/index.js';
|
|
4
|
+
import { zmdbAot as createAotPlugin, type UnpluginLike, type ZmdbAotOptions } from './unplugin/index.js';
|
|
5
|
+
|
|
6
|
+
export interface ConfiguredZmdbAotOptions extends ZmdbAotOptions {
|
|
7
|
+
/** Use this config path instead of discovery. */
|
|
8
|
+
readonly config?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create the AOT plugin, taking its project and naming strategy from `zmdb.config.ts`
|
|
13
|
+
* when the caller did not override them.
|
|
14
|
+
*/
|
|
15
|
+
export async function zmdbAot(options: ConfiguredZmdbAotOptions = {}): Promise<UnpluginLike> {
|
|
16
|
+
const { config: configPath, ...pluginOptions } = options;
|
|
17
|
+
const configCwd = options.cwd ?? (options.project === undefined ? undefined : dirname(options.project));
|
|
18
|
+
const config =
|
|
19
|
+
configPath === undefined
|
|
20
|
+
? await loadConfig({
|
|
21
|
+
...(configCwd === undefined ? {} : { cwd: configCwd }),
|
|
22
|
+
optional: true,
|
|
23
|
+
})
|
|
24
|
+
: await loadConfig({
|
|
25
|
+
...(configCwd === undefined ? {} : { cwd: configCwd }),
|
|
26
|
+
path: configPath,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return createAotPlugin({
|
|
30
|
+
...pluginOptions,
|
|
31
|
+
...(pluginOptions.project === undefined && config !== undefined ? { project: config.project } : {}),
|
|
32
|
+
...(pluginOptions.cwd === undefined && config !== undefined ? { cwd: dirname(config.configPath) } : {}),
|
|
33
|
+
...(pluginOptions.naming === undefined && config !== undefined ? { naming: config.resolvedNaming } : {}),
|
|
34
|
+
});
|
|
35
|
+
}
|