@kequtech/tenon-orm 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +16 -20
  3. package/dist/affected-rows.d.ts +1 -1
  4. package/dist/affected-rows.js +6 -6
  5. package/dist/baseline.d.ts +1 -1
  6. package/dist/baseline.js +5 -5
  7. package/dist/cli.js +18 -39
  8. package/dist/client.js +1 -1
  9. package/dist/connection.d.ts +18 -13
  10. package/dist/connection.js +14 -43
  11. package/dist/database.js +4 -2
  12. package/dist/describe.d.ts +3 -0
  13. package/dist/describe.js +21 -0
  14. package/dist/driver/array.d.ts +5 -0
  15. package/dist/driver/array.js +70 -0
  16. package/dist/driver/lifecycle.d.ts +15 -0
  17. package/dist/driver/lifecycle.js +57 -0
  18. package/dist/driver/session.d.ts +8 -0
  19. package/dist/driver/session.js +92 -0
  20. package/dist/driver/types.d.ts +10 -0
  21. package/dist/driver/types.js +49 -0
  22. package/dist/generate.js +7 -7
  23. package/dist/generation/catalog.d.ts +2 -2
  24. package/dist/generation/catalog.js +4 -4
  25. package/dist/generation/output.d.ts +2 -0
  26. package/dist/generation/output.js +35 -0
  27. package/dist/generation/schema-summary.d.ts +3 -3
  28. package/dist/generation/schema-summary.js +11 -11
  29. package/dist/generation/table-types.js +2 -52
  30. package/dist/index.d.ts +4 -1
  31. package/dist/index.js +1 -0
  32. package/dist/inspection/catalog.d.ts +25 -0
  33. package/dist/inspection/catalog.js +29 -0
  34. package/dist/inspection/cli.d.ts +22 -0
  35. package/dist/inspection/cli.js +34 -0
  36. package/dist/inspection/connection.d.ts +4 -0
  37. package/dist/inspection/connection.js +44 -0
  38. package/dist/inspection/options.d.ts +15 -0
  39. package/dist/inspection/options.js +49 -0
  40. package/dist/inspection/result.d.ts +4 -0
  41. package/dist/inspection/result.js +90 -0
  42. package/dist/inspection/types.d.ts +74 -0
  43. package/dist/inspection/types.js +1 -0
  44. package/dist/migrations/baseline-sql.d.ts +1 -1
  45. package/dist/migrations/baseline-sql.js +1 -1
  46. package/dist/migrations/connection.d.ts +2 -3
  47. package/dist/migrations/connection.js +15 -13
  48. package/dist/migrations/history.d.ts +4 -4
  49. package/dist/migrations/history.js +5 -5
  50. package/dist/migrations.js +7 -7
  51. package/dist/mutations.d.ts +0 -5
  52. package/dist/mutations.js +33 -41
  53. package/dist/query-options.d.ts +2 -2
  54. package/dist/query-options.js +4 -4
  55. package/dist/query.d.ts +3 -4
  56. package/dist/query.js +2 -2
  57. package/dist/sql.d.ts +1 -1
  58. package/dist/sql.js +1 -1
  59. package/dist/transaction-sql.js +1 -1
  60. package/dist/transaction.d.ts +2 -2
  61. package/dist/transaction.js +14 -13
  62. package/dist/types.d.ts +4 -4
  63. package/docs/adoption-guide.md +39 -76
  64. package/docs/agent-guide.md +82 -111
  65. package/docs/human-guide.md +42 -56
  66. package/docs/mutations.md +13 -13
  67. package/docs/query-inspection.md +128 -0
  68. package/docs/reference.md +81 -294
  69. package/package.json +10 -31
  70. package/dist/safeql/composition.d.ts +0 -9
  71. package/dist/safeql/composition.js +0 -168
  72. package/dist/safeql/eslint.d.ts +0 -2
  73. package/dist/safeql/eslint.js +0 -50
  74. package/dist/safeql/mutations.d.ts +0 -3
  75. package/dist/safeql/mutations.js +0 -74
  76. package/dist/safeql/plugin.d.ts +0 -3
  77. package/dist/safeql/plugin.js +0 -43
  78. package/dist/safeql/static.d.ts +0 -10
  79. package/dist/safeql/static.js +0 -59
  80. package/dist/safeql.d.ts +0 -6
  81. package/dist/safeql.js +0 -7
@@ -1,168 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { basename, dirname, join } from 'node:path';
3
- import ts from 'typescript';
4
- import { joinSeparator, quote } from '../sql.js';
5
- import { renderMutation } from './mutations.js';
6
- import { CompositionError, initializer, literalString, unwrap } from './static.js';
7
- export { CompositionError } from './static.js';
8
- /** Recognize Tenon's declarations without claiming another library's SQL tags. */
9
- const tenonModules = new Map();
10
- function isTenonModule(filename) {
11
- const cached = tenonModules.get(filename);
12
- if (cached !== undefined)
13
- return cached;
14
- let matches = false;
15
- if (['query.ts', 'query.d.ts'].includes(basename(filename)) &&
16
- ['src', 'dist'].includes(basename(dirname(filename)))) {
17
- try {
18
- const manifest = JSON.parse(readFileSync(join(dirname(filename), '..', 'package.json'), 'utf8'));
19
- matches =
20
- typeof manifest === 'object' &&
21
- manifest !== null &&
22
- 'name' in manifest &&
23
- manifest.name === '@kequtech/tenon-orm';
24
- }
25
- catch {
26
- // An unrelated or unavailable declaration is not a Tenon export.
27
- }
28
- }
29
- tenonModules.set(filename, matches);
30
- return matches;
31
- }
32
- export function isExport(node, checker, name) {
33
- // Call signatures retain their declaration through local aliases, object properties,
34
- // and typeof annotations. Symbol names alone only recognize direct imports.
35
- return checker
36
- .getTypeAtLocation(node)
37
- .getCallSignatures()
38
- .some((signature) => {
39
- const declaration = signature.declaration;
40
- return (declaration &&
41
- ts.isFunctionDeclaration(declaration) &&
42
- declaration.name?.text === (name === 'sql' ? 'createQuery' : name) &&
43
- isTenonModule(declaration.getSourceFile().fileName));
44
- });
45
- }
46
- export function isFragment(type) {
47
- return type.isUnion()
48
- ? type.types.some(isFragment)
49
- : type.getProperty('__tenonFragment') !== undefined;
50
- }
51
- /** Reconstruct only immutable fragments and literal join lists. Never execute application code. */
52
- export function renderFragment(node, checker, seen = new Set()) {
53
- node = unwrap(node);
54
- if (seen.has(node))
55
- throw new CompositionError('Circular fragment reference.');
56
- const visited = new Set(seen).add(node);
57
- if (ts.isIdentifier(node)) {
58
- const value = initializer(node, checker);
59
- if (value)
60
- return renderFragment(value, checker, visited);
61
- }
62
- if (ts.isTaggedTemplateExpression(node) &&
63
- ts.isPropertyAccessExpression(node.tag) &&
64
- node.tag.name.text === 'fragment' &&
65
- isExport(node.tag.expression, checker, 'sql')) {
66
- const template = node.template;
67
- if (ts.isNoSubstitutionTemplateLiteral(template))
68
- return template.text;
69
- let text = template.head.text;
70
- for (const span of template.templateSpans) {
71
- text += renderValue(span.expression, checker, visited) + span.literal.text;
72
- }
73
- return text;
74
- }
75
- if (ts.isCallExpression(node)) {
76
- for (const kind of ['insertValues', 'assignments']) {
77
- if (isExport(node.expression, checker, kind))
78
- return renderMutation(kind, node, checker);
79
- }
80
- }
81
- if (ts.isCallExpression(node) &&
82
- ts.isPropertyAccessExpression(node.expression) &&
83
- node.expression.name.text === 'join' &&
84
- isExport(node.expression.expression, checker, 'sql')) {
85
- const [items, separator] = node.arguments;
86
- const list = items && unwrap(items);
87
- if (!list ||
88
- !ts.isArrayLiteralExpression(list) ||
89
- !list.elements.length ||
90
- !separator ||
91
- node.arguments.length !== 2) {
92
- throw new CompositionError('sql.join needs a nonempty literal array and a separator for static checking.');
93
- }
94
- const value = literalString(separator, checker);
95
- if (value === undefined) {
96
- throw new CompositionError('sql.join separators must be literal strings or const strings.');
97
- }
98
- let between;
99
- try {
100
- between = joinSeparator(value);
101
- }
102
- catch (error) {
103
- if (!(error instanceof Error))
104
- throw error;
105
- throw new CompositionError(error.message);
106
- }
107
- return list.elements.map((item) => renderFragment(item, checker, visited)).join(between);
108
- }
109
- if (ts.isCallExpression(node)) {
110
- throw new CompositionError(`Function-generated fragments from ${node.expression.getText()}() are not analyzed; the checker does not inspect function bodies.`);
111
- }
112
- if (ts.isConditionalExpression(node)) {
113
- throw new CompositionError('Conditional fragments are not analyzed; the checker does not choose JavaScript branches, even for literal conditions.');
114
- }
115
- if (ts.isIdentifier(node)) {
116
- throw new CompositionError(`Fragment "${node.text}" has no inspectable const initializer; mutable and runtime-supplied fragments are not analyzed.`);
117
- }
118
- throw new CompositionError('Only constant sql.fragment templates, literal sql.join arrays, and official mutation helpers are supported. Function-generated fragments and conditional SQL structure are not analyzed.');
119
- }
120
- export function renderIdentifier(node, checker, seen = new Set()) {
121
- node = unwrap(node);
122
- if (seen.has(node))
123
- throw new CompositionError('Circular identifier reference.');
124
- const visited = new Set(seen).add(node);
125
- if (ts.isIdentifier(node)) {
126
- const value = initializer(node, checker);
127
- if (value)
128
- return renderIdentifier(value, checker, visited);
129
- }
130
- if (ts.isCallExpression(node) &&
131
- isExport(node.expression, checker, 'identifier') &&
132
- node.arguments.length) {
133
- return node.arguments
134
- .map((part) => {
135
- const value = literalString(part, checker);
136
- if (value === undefined) {
137
- throw new CompositionError('Identifier names must be literal strings or const strings.');
138
- }
139
- try {
140
- return quote(value);
141
- }
142
- catch (error) {
143
- if (!(error instanceof Error))
144
- throw error;
145
- throw new CompositionError(error.message);
146
- }
147
- })
148
- .join('.');
149
- }
150
- throw new CompositionError('The identifier is selected at runtime.');
151
- }
152
- export function isIdentifierType(type) {
153
- if (type.isUnion())
154
- return type.types.some(isIdentifierType);
155
- const symbol = type.getSymbol();
156
- return (symbol?.getName() === 'Identifier' &&
157
- Boolean(symbol.declarations?.some((declaration) => isTenonModule(declaration.getSourceFile().fileName))));
158
- }
159
- export function renderValue(node, checker, seen = new Set()) {
160
- const type = checker.getTypeAtLocation(node);
161
- if (isFragment(type))
162
- return renderFragment(node, checker, seen);
163
- if (isIdentifierType(type))
164
- return renderIdentifier(node, checker, seen);
165
- // Match runtime SQL: PostgreSQL infers parameter types from SQL context, never
166
- // from JavaScript types. SafeQL numbers these tokens, respecting quotes/comments.
167
- return '$N';
168
- }
@@ -1,2 +0,0 @@
1
- import type { ESLint } from 'eslint';
2
- export declare const eslintPlugin: ESLint.Plugin;
@@ -1,50 +0,0 @@
1
- import ts from 'typescript';
2
- import { CompositionError, isExport, renderValue } from './composition.js';
3
- export const eslintPlugin = {
4
- rules: {
5
- 'check-composition': {
6
- meta: {
7
- type: 'problem',
8
- docs: { description: 'Report Tenon queries that cannot be statically checked.' },
9
- schema: [],
10
- },
11
- create(context) {
12
- // ESLint leaves parser services untyped; require the TypeScript parser's public services.
13
- const { program, esTreeNodeToTSNodeMap: nodes } = context.sourceCode.parserServices;
14
- if (!program || !nodes) {
15
- throw new Error('tenon/check-composition requires type-aware TypeScript parsing. Enable parserOptions.project or projectService.');
16
- }
17
- const checker = program.getTypeChecker();
18
- return {
19
- TaggedTemplateExpression(node) {
20
- const template = nodes.get(node);
21
- if (!template ||
22
- !ts.isTaggedTemplateExpression(template) ||
23
- !isExport(template.tag, checker, 'sql'))
24
- return;
25
- if (node.quasi.quasis.some(({ value }) => value.raw !== value.cooked)) {
26
- context.report({
27
- node,
28
- message: new CompositionError('JavaScript escapes in an outer sql template would be checked as different SQL. Use literal newlines and bound string values instead.').message,
29
- });
30
- return;
31
- }
32
- if (ts.isTemplateExpression(template.template)) {
33
- for (const [index, span] of template.template.templateSpans.entries()) {
34
- try {
35
- renderValue(span.expression, checker);
36
- }
37
- catch (error) {
38
- if (!(error instanceof CompositionError))
39
- throw error;
40
- context.report({ node: node.quasi.expressions[index], message: error.message });
41
- return;
42
- }
43
- }
44
- }
45
- },
46
- };
47
- },
48
- },
49
- },
50
- };
@@ -1,3 +0,0 @@
1
- import ts from 'typescript';
2
- /** Understand the official mutation contract, never the implementation of a JavaScript helper. */
3
- export declare function renderMutation(kind: 'insertValues' | 'assignments', node: ts.CallExpression, checker: ts.TypeChecker): string;
@@ -1,74 +0,0 @@
1
- import ts from 'typescript';
2
- import { assignmentLayout, insertLayout, mutationColumns } from '../mutations.js';
3
- import { CompositionError, isUndefined, literalString, unwrap } from './static.js';
4
- const parameter = 0;
5
- function fields(input) {
6
- const node = unwrap(input);
7
- if (!ts.isObjectLiteralExpression(node)) {
8
- throw new CompositionError('Mutation inputs need inline object literals or an explicit literal column list.');
9
- }
10
- const result = new Map();
11
- for (const property of node.properties) {
12
- if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) {
13
- throw new CompositionError('Mutation object spreads, accessors, and methods need an explicit literal column list.');
14
- }
15
- const name = property.name;
16
- if (!ts.isIdentifier(name) && !ts.isStringLiteral(name) && !ts.isNumericLiteral(name)) {
17
- throw new CompositionError('Mutation keys must be literal names; use an explicit literal column list.');
18
- }
19
- if (name.text === '__proto__')
20
- throw new CompositionError('Prototype-setting object literals are unsupported mutation inputs.');
21
- if (result.has(name.text))
22
- throw new CompositionError('Duplicate mutation object keys are unsupported.');
23
- result.set(name.text, ts.isPropertyAssignment(property) ? property.initializer : property.name);
24
- }
25
- return result;
26
- }
27
- /** Understand the official mutation contract, never the implementation of a JavaScript helper. */
28
- export function renderMutation(kind, node, checker) {
29
- if (node.questionDotToken ||
30
- node.arguments.some(ts.isSpreadElement) ||
31
- node.arguments.length < 1 ||
32
- node.arguments.length > 2) {
33
- throw new CompositionError(`sql.${kind} requires an input and an optional literal column list.`);
34
- }
35
- try {
36
- const [argument, selected] = node.arguments;
37
- let columns;
38
- let rows;
39
- if (selected && !isUndefined(selected, checker)) {
40
- const list = unwrap(selected);
41
- if (!ts.isArrayLiteralExpression(list))
42
- throw new CompositionError('Mutation columns must be an inline literal array.');
43
- columns = mutationColumns(list.elements.map((item) => {
44
- const column = literalString(item, checker);
45
- if (column === undefined)
46
- throw new CompositionError('Mutation columns must be literal strings or const strings.');
47
- return column;
48
- }));
49
- // A fixed column contract covers any nonempty batch and every omitted/default cell.
50
- // Check all selected columns conservatively; no runtime data or JS types become SQL.
51
- rows = [columns.map(() => parameter)];
52
- }
53
- else {
54
- const value = unwrap(argument);
55
- const objects = kind === 'insertValues' && ts.isArrayLiteralExpression(value)
56
- ? value.elements.map(fields)
57
- : [fields(value)];
58
- columns = [
59
- ...new Set(objects.flatMap((row) => [...row].filter(([, value]) => !isUndefined(value, checker)).map(([key]) => key))),
60
- ];
61
- rows = objects.map((row) => columns.map((column) => {
62
- const cell = row.get(column);
63
- return !cell || isUndefined(cell, checker) ? undefined : parameter;
64
- }));
65
- }
66
- const parts = kind === 'insertValues' ? insertLayout(columns, rows) : assignmentLayout(columns, rows[0]);
67
- return parts.map((part) => (typeof part === 'string' ? part : '$N')).join('');
68
- }
69
- catch (error) {
70
- if (!(error instanceof Error))
71
- throw error;
72
- throw new CompositionError(`sql.${kind}: ${error instanceof CompositionError ? error.detail : error.message}`);
73
- }
74
- }
@@ -1,3 +0,0 @@
1
- import type { SafeQLPlugin } from '@ts-safeql/plugin-utils';
2
- /** Loaded through the exported SafeQL plugin descriptor. */
3
- export declare function factory(): SafeQLPlugin;
@@ -1,43 +0,0 @@
1
- import ts from 'typescript';
2
- import { CompositionError, isExport, renderValue } from './composition.js';
3
- /** Loaded through the exported SafeQL plugin descriptor. */
4
- export function factory() {
5
- return {
6
- name: 'tenon',
7
- connectionDefaults: {
8
- inferLiterals: false,
9
- overrides: {
10
- types: {
11
- int8: 'string',
12
- numeric: 'string',
13
- json: 'unknown',
14
- jsonb: 'unknown',
15
- date: 'Date | number',
16
- timestamp: 'Date | number',
17
- timestamptz: 'Date | number',
18
- },
19
- },
20
- },
21
- onTarget({ node, context }) {
22
- const template = context.parser.esTreeNodeToTSNodeMap.get(node);
23
- if (!ts.isTaggedTemplateExpression(template) ||
24
- !isExport(template.tag, context.checker, 'sql'))
25
- return false;
26
- // The companion ESLint rule reports unsupported forms. Skip their SQL analysis.
27
- if (node.quasi.quasis.some(({ value }) => value.raw !== value.cooked))
28
- return false;
29
- // Defer to SafeQL's configured targets, including per-connection tag selection.
30
- return undefined;
31
- },
32
- onExpression({ context }) {
33
- try {
34
- return renderValue(context.tsNode, context.checker);
35
- }
36
- catch (error) {
37
- if (!(error instanceof CompositionError))
38
- throw error;
39
- return false;
40
- }
41
- },
42
- };
43
- }
@@ -1,10 +0,0 @@
1
- import ts from 'typescript';
2
- export declare class CompositionError extends Error {
3
- readonly detail: string;
4
- readonly name = "TenonCompositionError";
5
- constructor(detail: string);
6
- }
7
- export declare function unwrap(node: ts.Node): ts.Node;
8
- export declare function initializer(node: ts.Identifier, checker: ts.TypeChecker): ts.Expression | undefined;
9
- export declare function literalString(node: ts.Node, checker: ts.TypeChecker): string | undefined;
10
- export declare function isUndefined(node: ts.Node, checker: ts.TypeChecker): boolean;
@@ -1,59 +0,0 @@
1
- import ts from 'typescript';
2
- export class CompositionError extends Error {
3
- detail;
4
- name = 'TenonCompositionError';
5
- constructor(detail) {
6
- super(`Tenon cannot statically check this composition: ${detail} Use a static query/fragment, or an explicit lint suppression with database tests for every runtime variant.`);
7
- this.detail = detail;
8
- }
9
- }
10
- export function unwrap(node) {
11
- while (ts.isParenthesizedExpression(node) ||
12
- ts.isAsExpression(node) ||
13
- ts.isSatisfiesExpression(node) ||
14
- ts.isNonNullExpression(node) ||
15
- ts.isTypeAssertionExpression(node))
16
- node = node.expression;
17
- return node;
18
- }
19
- export function initializer(node, checker) {
20
- let symbol = checker.getSymbolAtLocation(node);
21
- if (symbol && symbol.flags & ts.SymbolFlags.Alias)
22
- symbol = checker.getAliasedSymbol(symbol);
23
- const declaration = symbol?.valueDeclaration;
24
- return declaration &&
25
- ts.isVariableDeclaration(declaration) &&
26
- ts.isVariableDeclarationList(declaration.parent) &&
27
- declaration.parent.flags & ts.NodeFlags.Const
28
- ? declaration.initializer
29
- : undefined;
30
- }
31
- /** Follow immutable const aliases only. Never inspect functions or evaluate expressions. */
32
- function constant(node, checker) {
33
- const seen = new Set();
34
- node = unwrap(node);
35
- while (ts.isIdentifier(node)) {
36
- if (seen.has(node))
37
- throw new CompositionError('Circular const reference.');
38
- if (seen.size >= 1000)
39
- throw new CompositionError('Const resolution exceeds 1000 references.');
40
- seen.add(node);
41
- const value = initializer(node, checker);
42
- if (!value)
43
- break;
44
- node = unwrap(value);
45
- }
46
- return node;
47
- }
48
- export function literalString(node, checker) {
49
- const value = constant(node, checker);
50
- return ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)
51
- ? value.text
52
- : undefined;
53
- }
54
- export function isUndefined(node, checker) {
55
- const value = constant(node, checker);
56
- return (ts.isIdentifier(value) &&
57
- value.text === 'undefined' &&
58
- !checker.getSymbolAtLocation(value)?.declarations?.length);
59
- }
package/dist/safeql.d.ts DELETED
@@ -1,6 +0,0 @@
1
- /** Add to a connection's plugins in normal SafeQL configuration. */
2
- export declare const safeqlPlugin: {
3
- package: string;
4
- };
5
- /** Register as `tenon` in ESLint and enable `tenon/check-composition`. */
6
- export { eslintPlugin } from './safeql/eslint.js';
package/dist/safeql.js DELETED
@@ -1,7 +0,0 @@
1
- import { fileURLToPath } from 'node:url';
2
- /** Add to a connection's plugins in normal SafeQL configuration. */
3
- export const safeqlPlugin = {
4
- package: fileURLToPath(new URL('./safeql/plugin.js', import.meta.url)),
5
- };
6
- /** Register as `tenon` in ESLint and enable `tenon/check-composition`. */
7
- export { eslintPlugin } from './safeql/eslint.js';