@kequtech/tenon-orm 0.3.1 → 0.4.2
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/CHANGELOG.md +66 -0
- package/README.md +81 -26
- package/dist/affected-rows.js +0 -1
- package/dist/baseline.js +3 -5
- package/dist/check/contracts.d.ts +6 -0
- package/dist/check/contracts.js +206 -0
- package/dist/check/project.d.ts +18 -0
- package/dist/check/project.js +224 -0
- package/dist/check/source.d.ts +51 -0
- package/dist/check/source.js +629 -0
- package/dist/check/types.d.ts +36 -0
- package/dist/check/types.js +1 -0
- package/dist/check.d.ts +4 -0
- package/dist/check.js +147 -0
- package/dist/cli.js +40 -27
- package/dist/connection.d.ts +5 -12
- package/dist/connection.js +12 -121
- package/dist/database.js +10 -8
- package/dist/describe.js +10 -3
- package/dist/generate.d.ts +13 -3
- package/dist/generate.js +52 -25
- package/dist/generation/catalog.d.ts +5 -6
- package/dist/generation/catalog.js +7 -9
- package/dist/generation/output.js +2 -4
- package/dist/generation/render.d.ts +3 -0
- package/dist/generation/render.js +60 -0
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/inspection/catalog.d.ts +3 -6
- package/dist/inspection/catalog.js +9 -18
- package/dist/inspection/cli.d.ts +12 -0
- package/dist/inspection/cli.js +9 -4
- package/dist/inspection/connection.d.ts +2 -3
- package/dist/inspection/connection.js +20 -22
- package/dist/inspection/options.js +0 -6
- package/dist/inspection/result.d.ts +3 -2
- package/dist/inspection/result.js +13 -26
- package/dist/inspection/types.d.ts +6 -4
- package/dist/migrations/connection.d.ts +3 -3
- package/dist/migrations/connection.js +14 -21
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -9
- package/dist/query-options.d.ts +1 -1
- package/dist/query.js +1 -1
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +21 -77
- package/dist/types.d.ts +4 -1
- package/docs/adoption-guide.md +88 -111
- package/docs/agent-guide.md +94 -147
- package/docs/connections.md +185 -0
- package/docs/human-guide.md +133 -146
- package/docs/migrations.md +133 -0
- package/docs/mutations.md +110 -60
- package/docs/query-checking.md +199 -0
- package/docs/query-inspection.md +89 -58
- package/docs/reference.md +232 -437
- package/docs/schema-types.md +263 -0
- package/docs/sql.md +212 -0
- package/package.json +11 -7
- package/dist/driver/types.d.ts +0 -10
- package/dist/driver/types.js +0 -49
- package/dist/generation/table-types.d.ts +0 -2
- package/dist/generation/table-types.js +0 -96
- package/dist/inspection/protocol.d.ts +0 -7
- package/dist/inspection/protocol.js +0 -45
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { defaultCodecs } from '@kequtech/pgclear';
|
|
5
|
+
export function location(node) {
|
|
6
|
+
const source = node.getSourceFile();
|
|
7
|
+
const position = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
8
|
+
return {
|
|
9
|
+
file: resolve(source.fileName),
|
|
10
|
+
line: position.line + 1,
|
|
11
|
+
column: position.character + 1,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function symbol(checker, node) {
|
|
15
|
+
const found = node.parent && ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
|
|
16
|
+
? checker.getShorthandAssignmentValueSymbol(node.parent)
|
|
17
|
+
: checker.getSymbolAtLocation(node);
|
|
18
|
+
return found && found.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(found) : found;
|
|
19
|
+
}
|
|
20
|
+
/** Validate modules supplying representation types without selecting unrelated project files. */
|
|
21
|
+
function representationSources(checker, root) {
|
|
22
|
+
const sources = new Set();
|
|
23
|
+
const pending = [root];
|
|
24
|
+
while (pending.length) {
|
|
25
|
+
const source = pending.pop();
|
|
26
|
+
if (sources.has(source))
|
|
27
|
+
continue;
|
|
28
|
+
sources.add(source);
|
|
29
|
+
const visit = (node) => {
|
|
30
|
+
let reference;
|
|
31
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
|
32
|
+
reference = node.moduleSpecifier;
|
|
33
|
+
}
|
|
34
|
+
else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
|
35
|
+
reference = node.argument.literal;
|
|
36
|
+
}
|
|
37
|
+
else if (ts.isImportEqualsDeclaration(node) &&
|
|
38
|
+
ts.isExternalModuleReference(node.moduleReference)) {
|
|
39
|
+
reference = node.moduleReference.expression;
|
|
40
|
+
}
|
|
41
|
+
else if (ts.isCallExpression(node) &&
|
|
42
|
+
node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
43
|
+
reference = node.arguments[0];
|
|
44
|
+
}
|
|
45
|
+
if (reference && ts.isStringLiteralLike(reference)) {
|
|
46
|
+
const module = checker.getSymbolAtLocation(reference);
|
|
47
|
+
for (const declaration of module?.declarations ?? []) {
|
|
48
|
+
pending.push(declaration.getSourceFile());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
ts.forEachChild(node, visit);
|
|
52
|
+
};
|
|
53
|
+
visit(source);
|
|
54
|
+
}
|
|
55
|
+
return [...sources];
|
|
56
|
+
}
|
|
57
|
+
const representations = [
|
|
58
|
+
'unknown',
|
|
59
|
+
'string',
|
|
60
|
+
'number',
|
|
61
|
+
'bigint',
|
|
62
|
+
'boolean',
|
|
63
|
+
'globalThis.Uint8Array',
|
|
64
|
+
'globalThis.Date',
|
|
65
|
+
'import("@kequtech/tenon-orm").TemporalInput',
|
|
66
|
+
...['unknown', 'string', 'number', 'bigint', 'boolean', 'globalThis.Uint8Array'].map((type) => `PgArray<${type}>`),
|
|
67
|
+
];
|
|
68
|
+
export function loadProject(options) {
|
|
69
|
+
const match = options.target?.match(/^(.*):(\d+)$/);
|
|
70
|
+
const filename = options.target === undefined ? undefined : resolve(match?.[1] ?? options.target);
|
|
71
|
+
const line = match === null || match === undefined ? undefined : Number(match[2]);
|
|
72
|
+
if (line !== undefined && (!Number.isSafeInteger(line) || line < 1)) {
|
|
73
|
+
throw new Error('The query line must be a positive, one-based integer.');
|
|
74
|
+
}
|
|
75
|
+
const configPath = options.project === undefined
|
|
76
|
+
? ts.findConfigFile(filename ? dirname(filename) : process.cwd(), ts.sys.fileExists)
|
|
77
|
+
: ts.sys.directoryExists(resolve(options.project))
|
|
78
|
+
? resolve(options.project, 'tsconfig.json')
|
|
79
|
+
: resolve(options.project);
|
|
80
|
+
if (!configPath)
|
|
81
|
+
throw new Error('No tsconfig.json found. Pass --project with the project configuration.');
|
|
82
|
+
const config = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
83
|
+
if (config.error)
|
|
84
|
+
throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n'));
|
|
85
|
+
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, dirname(configPath), undefined, configPath);
|
|
86
|
+
if (parsed.errors.length) {
|
|
87
|
+
throw new Error(parsed.errors
|
|
88
|
+
.map((error) => ts.flattenDiagnosticMessageText(error.messageText, '\n'))
|
|
89
|
+
.join('\n'));
|
|
90
|
+
}
|
|
91
|
+
if (!(parsed.options.strictNullChecks ?? parsed.options.strict)) {
|
|
92
|
+
throw new Error('Query checking requires strictNullChecks in the TypeScript project.');
|
|
93
|
+
}
|
|
94
|
+
const custom = Object.entries(options.readTypes ?? {});
|
|
95
|
+
for (const [oid, type] of custom) {
|
|
96
|
+
if (!Number.isInteger(Number(oid)) ||
|
|
97
|
+
Number(oid) < 1 ||
|
|
98
|
+
Number(oid) > 0xffffffff ||
|
|
99
|
+
typeof type !== 'string' ||
|
|
100
|
+
!type.trim()) {
|
|
101
|
+
throw new Error('readTypes requires PostgreSQL OIDs and nonempty TypeScript type expressions.');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const expressions = [
|
|
105
|
+
...new Set([
|
|
106
|
+
...representations,
|
|
107
|
+
...(options.codecs ?? defaultCodecs).typescriptTypes(),
|
|
108
|
+
...custom.map(([, type]) => type),
|
|
109
|
+
]),
|
|
110
|
+
];
|
|
111
|
+
const virtualPath = resolve(dirname(configPath), `.tenon-check-${randomUUID()}.ts`);
|
|
112
|
+
const virtualSource = [
|
|
113
|
+
'type PgArray<T> = readonly (T | null | PgArray<T>)[];',
|
|
114
|
+
...expressions.map((type, index) => `export type Read${index} = ${type};`),
|
|
115
|
+
'export type Parameter = import("@kequtech/tenon-orm").Parameter;',
|
|
116
|
+
].join('\n');
|
|
117
|
+
// Emission layout must not constrain the in-memory representation module.
|
|
118
|
+
const compilerOptions = { ...parsed.options, noEmit: true };
|
|
119
|
+
delete compilerOptions.rootDir;
|
|
120
|
+
const host = ts.createCompilerHost(compilerOptions);
|
|
121
|
+
const readFile = host.readFile.bind(host);
|
|
122
|
+
const fileExists = host.fileExists.bind(host);
|
|
123
|
+
host.readFile = (path) => (resolve(path) === virtualPath ? virtualSource : readFile(path));
|
|
124
|
+
host.fileExists = (path) => resolve(path) === virtualPath || fileExists(path);
|
|
125
|
+
const program = ts.createProgram({
|
|
126
|
+
rootNames: [...parsed.fileNames, virtualPath],
|
|
127
|
+
options: compilerOptions,
|
|
128
|
+
host,
|
|
129
|
+
...(parsed.projectReferences ? { projectReferences: parsed.projectReferences } : {}),
|
|
130
|
+
});
|
|
131
|
+
const checker = program.getTypeChecker();
|
|
132
|
+
const helper = program.getSourceFile(virtualPath);
|
|
133
|
+
const types = new Map();
|
|
134
|
+
for (const statement of helper.statements) {
|
|
135
|
+
if (ts.isTypeAliasDeclaration(statement) && statement.name.text.startsWith('Read')) {
|
|
136
|
+
types.set(expressions[Number(statement.name.text.slice(4))], checker.getTypeFromTypeNode(statement.type));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const parameterNode = helper.statements.find((node) => ts.isTypeAliasDeclaration(node) && node.name.text === 'Parameter');
|
|
140
|
+
const parameterType = checker.getTypeFromTypeNode(parameterNode.type);
|
|
141
|
+
const helperErrors = representationSources(checker, helper).flatMap((source) => [
|
|
142
|
+
...program.getSyntacticDiagnostics(source),
|
|
143
|
+
...program.getSemanticDiagnostics(source),
|
|
144
|
+
]);
|
|
145
|
+
if (helperErrors.length) {
|
|
146
|
+
throw new Error(`Cannot resolve checker representation types: ${helperErrors.map((error) => ts.flattenDiagnosticMessageText(error.messageText, '\n')).join('\n')}`);
|
|
147
|
+
}
|
|
148
|
+
const files = program
|
|
149
|
+
.getSourceFiles()
|
|
150
|
+
.filter((source) => !source.isDeclarationFile &&
|
|
151
|
+
resolve(source.fileName) !== virtualPath &&
|
|
152
|
+
!program.isSourceFileFromExternalLibrary(source));
|
|
153
|
+
const selected = filename === undefined
|
|
154
|
+
? files
|
|
155
|
+
: files.filter((source) => resolve(source.fileName) === filename);
|
|
156
|
+
if (filename !== undefined && !selected.length)
|
|
157
|
+
throw new Error('The target file is not a source file in this TypeScript project. Check --project and tsconfig inclusion.');
|
|
158
|
+
const diagnostics = [];
|
|
159
|
+
for (const error of [...program.getOptionsDiagnostics(), ...program.getGlobalDiagnostics()]) {
|
|
160
|
+
if (error.category === ts.DiagnosticCategory.Error)
|
|
161
|
+
diagnostics.push({
|
|
162
|
+
file: resolve(configPath),
|
|
163
|
+
line: 1,
|
|
164
|
+
column: 1,
|
|
165
|
+
kind: 'mismatch',
|
|
166
|
+
code: 'typescript-configuration',
|
|
167
|
+
message: ts.flattenDiagnosticMessageText(error.messageText, '\n'),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
for (const source of selected) {
|
|
171
|
+
for (const error of [
|
|
172
|
+
...program.getSyntacticDiagnostics(source),
|
|
173
|
+
...program.getSemanticDiagnostics(source),
|
|
174
|
+
]) {
|
|
175
|
+
if (error.category !== ts.DiagnosticCategory.Error)
|
|
176
|
+
continue;
|
|
177
|
+
const position = source.getLineAndCharacterOfPosition(error.start ?? 0);
|
|
178
|
+
diagnostics.push({
|
|
179
|
+
file: resolve(source.fileName),
|
|
180
|
+
line: position.line + 1,
|
|
181
|
+
column: position.character + 1,
|
|
182
|
+
kind: 'mismatch',
|
|
183
|
+
code: 'typescript-error',
|
|
184
|
+
message: ts.flattenDiagnosticMessageText(error.messageText, '\n'),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
program,
|
|
190
|
+
checker,
|
|
191
|
+
types,
|
|
192
|
+
parameterType,
|
|
193
|
+
files,
|
|
194
|
+
selected,
|
|
195
|
+
line,
|
|
196
|
+
project: resolve(configPath),
|
|
197
|
+
diagnostics,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** Resolve actual exported symbols, including aliases and barrel re-exports. */
|
|
201
|
+
export function librarySymbols(project) {
|
|
202
|
+
const found = new Map();
|
|
203
|
+
for (const source of project.program.getSourceFiles()) {
|
|
204
|
+
for (const statement of source.statements) {
|
|
205
|
+
if (!(ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) ||
|
|
206
|
+
!statement.moduleSpecifier ||
|
|
207
|
+
!ts.isStringLiteral(statement.moduleSpecifier))
|
|
208
|
+
continue;
|
|
209
|
+
const moduleName = statement.moduleSpecifier.text;
|
|
210
|
+
if (!['@kequtech/tenon-orm', '@kequtech/pgclear'].includes(moduleName))
|
|
211
|
+
continue;
|
|
212
|
+
const module = project.checker.getSymbolAtLocation(statement.moduleSpecifier);
|
|
213
|
+
if (!module)
|
|
214
|
+
continue;
|
|
215
|
+
for (const exported of project.checker.getExportsOfModule(module)) {
|
|
216
|
+
const target = exported.flags & ts.SymbolFlags.Alias
|
|
217
|
+
? project.checker.getAliasedSymbol(exported)
|
|
218
|
+
: exported;
|
|
219
|
+
found.set(`${moduleName}:${exported.name}`, target);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return found;
|
|
224
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { type Project } from './project.js';
|
|
3
|
+
import type { CheckDiagnostic } from './types.js';
|
|
4
|
+
export declare const variantLimit = 16;
|
|
5
|
+
type QueryNode = ts.TaggedTemplateExpression | ts.CallExpression;
|
|
6
|
+
export interface Binding {
|
|
7
|
+
readonly node: ts.Expression;
|
|
8
|
+
readonly type: ts.Type;
|
|
9
|
+
readonly field?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface QueryVariant {
|
|
12
|
+
readonly text: string;
|
|
13
|
+
readonly parameters: readonly Binding[];
|
|
14
|
+
readonly parameterTypes: readonly number[];
|
|
15
|
+
readonly uncertainParameters: readonly number[];
|
|
16
|
+
}
|
|
17
|
+
export declare class Unsupported extends Error {
|
|
18
|
+
readonly node: ts.Node;
|
|
19
|
+
readonly code: string;
|
|
20
|
+
constructor(node: ts.Node, code: string, message: string);
|
|
21
|
+
}
|
|
22
|
+
/** A bounded interpreter for SQL structure, never for application values or function bodies. */
|
|
23
|
+
export declare class SourceAnalysis {
|
|
24
|
+
readonly project: Project;
|
|
25
|
+
private readonly libraries;
|
|
26
|
+
readonly issues: CheckDiagnostic[];
|
|
27
|
+
private readonly immutableArrays;
|
|
28
|
+
private readonly stableParameters;
|
|
29
|
+
private readonly unstableConditions;
|
|
30
|
+
constructor(project: Project, libraries: ReadonlyMap<string, ts.Symbol>);
|
|
31
|
+
role(input: ts.Expression, depth?: number): string | undefined;
|
|
32
|
+
query(node: ts.Node): node is QueryNode;
|
|
33
|
+
discover(source: ts.SourceFile): QueryNode[];
|
|
34
|
+
private fail;
|
|
35
|
+
private reference;
|
|
36
|
+
private text;
|
|
37
|
+
private array;
|
|
38
|
+
private closedReference;
|
|
39
|
+
private combine;
|
|
40
|
+
private conditional;
|
|
41
|
+
private template;
|
|
42
|
+
private mutation;
|
|
43
|
+
private containsUndefined;
|
|
44
|
+
private expression;
|
|
45
|
+
private codecOid;
|
|
46
|
+
private encoderOid;
|
|
47
|
+
private hints;
|
|
48
|
+
recover(node: QueryNode): QueryVariant[];
|
|
49
|
+
private withHints;
|
|
50
|
+
}
|
|
51
|
+
export type { QueryNode };
|