@an5/orm 1.0.8 → 1.1.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 (73) hide show
  1. package/README.md +174 -175
  2. package/dist/an5Metadata.js +64 -0
  3. package/dist/an5Orm.d.ts +92 -3
  4. package/dist/an5Orm.js +697 -206
  5. package/dist/config.d.ts +9 -1
  6. package/dist/config.js +13 -18
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/errors.js +1 -1
  9. package/dist/generator/src/api.d.ts +35 -0
  10. package/dist/generator/src/api.js +58 -0
  11. package/dist/generator/src/code-generator.d.ts +14 -13
  12. package/dist/generator/src/code-generator.js +249 -225
  13. package/dist/generator/src/config.d.ts +162 -0
  14. package/dist/generator/src/config.js +354 -0
  15. package/dist/generator/src/dialect.d.ts +88 -0
  16. package/dist/generator/src/dialect.js +318 -0
  17. package/dist/generator/src/dotnet-generator.d.ts +21 -11
  18. package/dist/generator/src/dotnet-generator.js +1053 -546
  19. package/dist/generator/src/field-types.d.ts +96 -0
  20. package/dist/generator/src/field-types.js +433 -0
  21. package/dist/generator/src/golang-generator.d.ts +36 -0
  22. package/dist/generator/src/golang-generator.js +1229 -0
  23. package/dist/generator/src/index.d.ts +1 -1
  24. package/dist/generator/src/index.js +137 -88
  25. package/dist/generator/src/metadata-generator.d.ts +11 -8
  26. package/dist/generator/src/metadata-generator.js +105 -90
  27. package/dist/generator/src/parser.d.ts +37 -11
  28. package/dist/generator/src/parser.js +208 -206
  29. package/dist/generator/src/push-apply.d.ts +37 -0
  30. package/dist/generator/src/push-apply.js +109 -0
  31. package/dist/generator/src/push-schema.d.ts +93 -0
  32. package/dist/generator/src/push-schema.js +189 -0
  33. package/dist/generator/src/python-generator.d.ts +37 -9
  34. package/dist/generator/src/python-generator.js +428 -105
  35. package/dist/generator/src/rust-generator.d.ts +38 -0
  36. package/dist/generator/src/rust-generator.js +971 -0
  37. package/dist/generator/src/suggest.d.ts +17 -0
  38. package/dist/generator/src/suggest.js +45 -0
  39. package/dist/generator/src/type-kinds.d.ts +27 -0
  40. package/dist/generator/src/type-kinds.js +72 -0
  41. package/dist/generator/src/types.d.ts +60 -27
  42. package/dist/generator/src/types.js +41 -2
  43. package/dist/index.d.ts +3 -2
  44. package/dist/index.js +19 -8
  45. package/dist/logger.js +1 -1
  46. package/dist/migration-core.d.ts +102 -0
  47. package/dist/migration-core.js +731 -0
  48. package/dist/orm.d.ts +147 -0
  49. package/dist/orm.js +1654 -0
  50. package/dist/provider-support.d.ts +27 -0
  51. package/dist/provider-support.js +50 -0
  52. package/dist/scripts/release-cli.d.ts +2 -2
  53. package/dist/scripts/release-cli.js +115 -115
  54. package/dist/sql-utils.d.ts +3 -1
  55. package/dist/sql-utils.js +94 -24
  56. package/generator/src/api.ts +45 -0
  57. package/generator/src/code-generator.ts +259 -230
  58. package/generator/src/config.ts +496 -0
  59. package/generator/src/dialect.ts +415 -0
  60. package/generator/src/dotnet-generator.ts +1071 -544
  61. package/generator/src/field-types.ts +473 -0
  62. package/generator/src/golang-generator.ts +1267 -0
  63. package/generator/src/index.ts +146 -116
  64. package/generator/src/metadata-generator.ts +116 -101
  65. package/generator/src/parser.ts +234 -202
  66. package/generator/src/push-apply.ts +148 -0
  67. package/generator/src/push-schema.ts +250 -0
  68. package/generator/src/python-generator.ts +480 -114
  69. package/generator/src/rust-generator.ts +974 -0
  70. package/generator/src/suggest.ts +41 -0
  71. package/generator/src/type-kinds.ts +78 -0
  72. package/generator/src/types.ts +77 -29
  73. package/package.json +32 -6
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Reading `.an5` files for `db:push`.
3
+ *
4
+ * `SchemaParser` and `parseSchemaText` cover the generators and `db:migrate`;
5
+ * this is the third reader, and it exists because push needs a shape of its own:
6
+ * raw `@default` expressions rather than rendered SQL, and the schema a table
7
+ * lands in. It is kept here rather than inline in `push.ts` so the mapping from
8
+ * schema text to a column can be tested without a database.
9
+ */
10
+ import {
11
+ FieldTypeIssue,
12
+ Provider,
13
+ readFieldLineHead,
14
+ resolveFieldType,
15
+ unknownFieldTypeMessage,
16
+ } from './field-types';
17
+
18
+ /** A column, as push needs it: the type as written plus what the dialect renders. */
19
+ export interface PushField {
20
+ name: string;
21
+ sqlType: string;
22
+ isOptional: boolean;
23
+ isId: boolean;
24
+ isUnique: boolean;
25
+ /** The `@default(...)` expression, not SQL: only the dialect knows the spelling. */
26
+ defaultExpr?: string | undefined;
27
+ }
28
+
29
+ export interface PushModel {
30
+ name: string;
31
+ tableName: string;
32
+ /** The schema to create the table in; `''` when the provider has none. */
33
+ schema: string;
34
+ /** Set only when the schema file named one, so a wrong one can be reported. */
35
+ declaredSchema?: string | undefined;
36
+ fields: PushField[];
37
+ /** `@@unique([a, b])`, as field lists. */
38
+ compoundUniques: PushIndex[];
39
+ /** `@@index([a, b])`, as field lists. */
40
+ indexes: PushIndex[];
41
+ }
42
+
43
+ /**
44
+ * The schema a model lands in when the schema file does not say: none, for every
45
+ * provider, so the name goes wherever the connection resolves.
46
+ *
47
+ * The parser used to default this to `dbo` and push followed it, which failed two
48
+ * ways: SQLite said "unknown database [dbo]", and a SQL Server login whose default
49
+ * schema was something else had push look for `[dbo].[widgets]`, miss the table it
50
+ * had created, and then fail with "there is already an object named ...".
51
+ */
52
+ export function defaultSchemaFor(_provider: Provider): string {
53
+ return '';
54
+ }
55
+
56
+ /**
57
+ * One `@@unique` or `@@index`, with the options `db:migrate` already understands.
58
+ *
59
+ * `name` matters as much as the fields: `map:` is how a schema asks for a specific
60
+ * artifact name, and if push invented its own the two commands would create two
61
+ * different constraints for the same directive — and the next migration would keep
62
+ * trying to add the one push never made.
63
+ */
64
+ export interface PushIndex {
65
+ fields: string[];
66
+ name?: string | undefined;
67
+ includeFields?: string[] | undefined;
68
+ filter?: string | undefined;
69
+ options?: string | undefined;
70
+ }
71
+
72
+ export interface PushSchema {
73
+ models: PushModel[];
74
+ /** Field types the provider does not have; empty when the schema is valid. */
75
+ issues: FieldTypeIssue[];
76
+ }
77
+
78
+ /** The field list of `@@unique([a, b], …)`, plus whatever options follow it. */
79
+ const DIRECTIVE = /^@@(?:unique|index)\(\s*\[([\w\s,]+)\]([^)]*)\)/;
80
+
81
+ function parseDirective(line: string, fields: string): PushIndex {
82
+ const options = line.match(DIRECTIVE)?.[2] ?? '';
83
+ const name = options.match(/\bmap\s*:\s*"([^"]+)"/)?.[1];
84
+ const include = options.match(/\binclude\s*:\s*\[([\w,\s]+)\]/)?.[1];
85
+ const filter = options.match(/\bfilter\s*:\s*"([^"]+)"/)?.[1];
86
+ const opts = options.match(/\boptions\s*:\s*"([^"]+)"/)?.[1];
87
+ return {
88
+ fields: fields.split(',').map((field) => field.trim()).filter(Boolean),
89
+ ...(name ? { name } : {}),
90
+ ...(include ? { includeFields: include.split(',').map((field) => field.trim()).filter(Boolean) } : {}),
91
+ ...(filter ? { filter } : {}),
92
+ ...(opts ? { options: opts } : {}),
93
+ };
94
+ }
95
+
96
+ /**
97
+ * The name a `@@unique` or `@@index` artifact gets.
98
+ *
99
+ * `map:` wins; otherwise the name is derived from the table and fields, which is what
100
+ * both commands have always done for an unnamed directive.
101
+ */
102
+ export function directiveName(
103
+ directive: PushIndex,
104
+ table: string,
105
+ kind: 'compound' | 'index',
106
+ position: number,
107
+ ): string {
108
+ if (directive.name) return safeIdentifierName(directive.name);
109
+ const base = safeIdentifierName(table);
110
+ return kind === 'compound'
111
+ ? `UQ_${base}_compound_${position}`
112
+ : `IX_${base}_${directive.fields.join('_')}`;
113
+ }
114
+
115
+ /**
116
+ * Reads schema text for push, validating every field type against the provider.
117
+ *
118
+ * All bad types come back in `issues` rather than as an exception, so the caller
119
+ * can print them as a group. A field whose type matches no table is only a
120
+ * mistake once the whole schema has been read: it may be a relation to a model
121
+ * declared further down.
122
+ */
123
+ export function parsePushSchema(text: string, provider: Provider): PushSchema {
124
+ const models: PushModel[] = [];
125
+ const issues: FieldTypeIssue[] = [];
126
+ const modelRefs: Array<{ model: string; field: string; type: string }> = [];
127
+ let current: PushModel | null = null;
128
+
129
+ for (const raw of text.split('\n')) {
130
+ const line = raw.trim();
131
+ if (!line || line.startsWith('//')) continue;
132
+
133
+ const header = line.match(/^model\s+(\w+)\s*\{/);
134
+ if (header) {
135
+ current = {
136
+ name: header[1],
137
+ tableName: header[1].toLowerCase() + 's',
138
+ schema: defaultSchemaFor(provider),
139
+ fields: [],
140
+ compoundUniques: [],
141
+ indexes: [],
142
+ };
143
+ models.push(current);
144
+ continue;
145
+ }
146
+
147
+ if (line === '}') {
148
+ current = null;
149
+ continue;
150
+ }
151
+ if (!current) continue;
152
+
153
+ if (line.startsWith('@@map')) {
154
+ const match = line.match(/@@map\("(.+)"\)/);
155
+ if (match) current.tableName = match[1];
156
+ continue;
157
+ }
158
+ if (line.startsWith('@@schema')) {
159
+ // `(.*)` rather than `(.+)`, because `@@schema("")` is how a model says "no
160
+ // schema" and has to survive as an empty string rather than being dropped as
161
+ // no match — which is how SQLite and MySQL schemas are written.
162
+ const match = line.match(/@@schema\("(.*)"\)/);
163
+ if (match) {
164
+ current.schema = match[1].trim();
165
+ current.declaredSchema = current.schema;
166
+ }
167
+ continue;
168
+ }
169
+ if (line.startsWith('@@unique') || line.startsWith('@@index')) {
170
+ const match = line.match(DIRECTIVE);
171
+ if (match) {
172
+ const definition = parseDirective(line, match[1] ?? '');
173
+ if (line.startsWith('@@unique')) current.compoundUniques.push(definition);
174
+ else current.indexes.push(definition);
175
+ }
176
+ continue;
177
+ }
178
+ if (line.startsWith('@@')) continue;
179
+
180
+ const head = readFieldLineHead(line);
181
+ if (!head) continue;
182
+
183
+ if (!resolveFieldType(head.type, provider)) {
184
+ modelRefs.push({ model: current.name, field: head.name, type: head.type });
185
+ continue;
186
+ }
187
+
188
+ // The type is kept as written (upper-cased) because that is what the database
189
+ // is told; which types exist is what `resolveFieldType` just checked.
190
+ current.fields.push({
191
+ name: head.name,
192
+ sqlType: head.type.toUpperCase(),
193
+ isOptional: head.isOptional,
194
+ isId: line.includes('@id'),
195
+ isUnique: line.includes('@unique'),
196
+ // The expression rather than SQL: `uuid()` and `autoincrement()` are spelled
197
+ // differently per provider, and only the dialect knows how. `@updatedAt` is
198
+ // `now()` under another name.
199
+ defaultExpr: line.match(/@default\((.*)\)/)?.[1]
200
+ ?? (line.includes('@updatedAt') ? 'now()' : undefined),
201
+ });
202
+ }
203
+
204
+ // A declared `dbo` on a provider without schemas would push to a database that
205
+ // does not exist there; SQLite answers "unknown database [dbo]", which says
206
+ // nothing about the schema file. Only an explicit declaration is worth reporting:
207
+ // an absent one now means "whatever the connection resolves", on every provider.
208
+ for (const model of models) {
209
+ if (model.declaredSchema === 'dbo' && provider !== 'mssql') {
210
+ issues.push({
211
+ path: model.name,
212
+ message:
213
+ `schema "dbo" does not exist in ${provider}; remove the @@schema directive or set it ` +
214
+ 'to the schema this database uses',
215
+ });
216
+ }
217
+ }
218
+
219
+ const modelNames = models.map((model) => model.name);
220
+ for (const ref of modelRefs) {
221
+ if (!modelNames.includes(ref.type)) {
222
+ issues.push({
223
+ path: `${ref.model}.${ref.field}`,
224
+ message: unknownFieldTypeMessage(ref.type, provider, modelNames),
225
+ });
226
+ }
227
+ }
228
+
229
+ return { models, issues };
230
+ }
231
+
232
+ /**
233
+ * The table name to create, `schema.table` unless the model cleared the schema.
234
+ *
235
+ * An empty schema has to stay empty: `[].[widgets]` is not valid SQL, which is the
236
+ * whole reason `@@schema("")` exists.
237
+ */
238
+ /**
239
+ * An identifier for a generated artifact name: letters, digits and underscores only.
240
+ *
241
+ * The name goes into DDL as an identifier, so anything else is replaced rather than
242
+ * quoted — `@@map("catalog entries")` must not produce a name the provider rejects.
243
+ */
244
+ export function safeIdentifierName(raw: string): string {
245
+ return raw.replace(/[^A-Za-z0-9_]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
246
+ }
247
+
248
+ export function qualifiedTableName(model: PushModel): string {
249
+ return model.schema ? `${model.schema}.${model.tableName}` : model.tableName;
250
+ }