@an5/orm 1.3.2 → 1.3.4
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/README.md +74 -11
- package/dist/cleanup.d.ts +1 -0
- package/dist/cleanup.js +177 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +147 -0
- package/dist/generator/src/attributes.d.ts +57 -0
- package/dist/generator/src/attributes.js +225 -0
- package/dist/generator/src/code-generator.js +49 -16
- package/dist/generator/src/config.d.ts +2 -2
- package/dist/generator/src/config.js +4 -3
- package/dist/generator/src/dialect.d.ts +52 -1
- package/dist/generator/src/dialect.js +136 -3
- package/dist/generator/src/dotnet-generator.js +67 -23
- package/dist/generator/src/golang-generator.d.ts +8 -0
- package/dist/generator/src/golang-generator.js +86 -21
- package/dist/generator/src/java-generator.d.ts +8 -0
- package/dist/generator/src/java-generator.js +108 -3
- package/dist/generator/src/kotlin-generator.js +51 -0
- package/dist/generator/src/parser.d.ts +1 -0
- package/dist/generator/src/parser.js +90 -35
- package/dist/generator/src/pull-schema.d.ts +54 -0
- package/dist/generator/src/pull-schema.js +116 -0
- package/dist/generator/src/push-apply.d.ts +4 -2
- package/dist/generator/src/push-apply.js +53 -4
- package/dist/generator/src/push-schema.d.ts +21 -9
- package/dist/generator/src/push-schema.js +132 -32
- package/dist/generator/src/python-generator.js +35 -22
- package/dist/generator/src/rust-generator.js +79 -74
- package/dist/generator/src/swift-generator.js +54 -3
- package/dist/generator/src/types.d.ts +10 -0
- package/dist/generator/src/view-order.d.ts +33 -0
- package/dist/generator/src/view-order.js +123 -0
- package/dist/migrate.d.ts +12 -0
- package/dist/migrate.js +442 -0
- package/dist/migration-core.d.ts +32 -3
- package/dist/migration-core.js +213 -43
- package/dist/provider-support.d.ts +0 -1
- package/dist/provider-support.js +0 -1
- package/dist/pull.d.ts +1 -0
- package/dist/pull.js +425 -0
- package/dist/push.d.ts +1 -0
- package/dist/push.js +93 -0
- package/dist/seed.d.ts +1 -0
- package/dist/seed.js +33 -0
- package/generator/src/attributes.ts +231 -0
- package/generator/src/code-generator.ts +47 -16
- package/generator/src/config.ts +5 -3
- package/generator/src/dialect.ts +188 -5
- package/generator/src/dotnet-generator.ts +68 -24
- package/generator/src/golang-generator.ts +89 -22
- package/generator/src/java-generator.ts +109 -3
- package/generator/src/kotlin-generator.ts +51 -0
- package/generator/src/parser.ts +102 -30
- package/generator/src/pull-schema.ts +127 -0
- package/generator/src/push-apply.ts +62 -5
- package/generator/src/push-schema.ts +160 -32
- package/generator/src/python-generator.ts +33 -20
- package/generator/src/rust-generator.ts +79 -73
- package/generator/src/swift-generator.ts +55 -4
- package/generator/src/types.ts +10 -0
- package/generator/src/view-order.ts +133 -0
- package/package.json +19 -14
package/dist/pull.js
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
require("dotenv/config");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const adapters_1 = require("@an5/adapters");
|
|
10
|
+
const config_1 = require("./generator/src/config");
|
|
11
|
+
const pull_schema_1 = require("./generator/src/pull-schema");
|
|
12
|
+
const provider_support_1 = require("./provider-support");
|
|
13
|
+
const rootDir = process.cwd();
|
|
14
|
+
const { config, outputs } = (0, config_1.loadConfig)();
|
|
15
|
+
const schemaDir = outputs.schemaDir;
|
|
16
|
+
let _adapter = null;
|
|
17
|
+
async function getDb() {
|
|
18
|
+
if (!_adapter) {
|
|
19
|
+
_adapter = new adapters_1.An5Adapter({ connectionString: (0, config_1.resolveConnectionString)(config, process.env, "db:pull") });
|
|
20
|
+
await _adapter.$connect();
|
|
21
|
+
}
|
|
22
|
+
return _adapter;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Prints why a command cannot run against this provider and exits.
|
|
26
|
+
*
|
|
27
|
+
* The decision and the wording live in `./provider-support`, which stays free of Node
|
|
28
|
+
* globals so it can be imported and tested anywhere; only the command files, which
|
|
29
|
+
* are Node programs, do the exiting.
|
|
30
|
+
*/
|
|
31
|
+
function refuseIfNotSqlServer(provider, command) {
|
|
32
|
+
const message = (0, provider_support_1.sqlServerOnlyError)(provider, command);
|
|
33
|
+
if (message === null)
|
|
34
|
+
return;
|
|
35
|
+
console.error(message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The block name `db:pull` used before it split snake_case and protected singular
|
|
40
|
+
* words (`Order_item`, `User_statu`). Kept only to match a schema that already
|
|
41
|
+
* contains those blocks.
|
|
42
|
+
*/
|
|
43
|
+
function legacyBlockName(objectName) {
|
|
44
|
+
return objectName.charAt(0).toUpperCase() + objectName.slice(1).replace(/s$/, "");
|
|
45
|
+
}
|
|
46
|
+
async function pull() {
|
|
47
|
+
// Reads sys.tables/sys.columns and writes MSSQL-shaped type names, so another
|
|
48
|
+
// provider would fail on the first catalog query with a driver error that says
|
|
49
|
+
// nothing about the connection string being wrong.
|
|
50
|
+
refuseIfNotSqlServer((0, config_1.providerFromConfig)(config, process.env), "db:pull");
|
|
51
|
+
console.log("🔍 Pulling schema from database...");
|
|
52
|
+
const tables = await (await getDb()).$queryRawUnsafe(`
|
|
53
|
+
SELECT s.name AS schemaName, t.name AS tableName
|
|
54
|
+
FROM sys.tables t
|
|
55
|
+
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
|
56
|
+
WHERE t.is_ms_shipped = 0
|
|
57
|
+
ORDER BY t.name
|
|
58
|
+
`);
|
|
59
|
+
// Load existing schema files
|
|
60
|
+
const fileContents = {};
|
|
61
|
+
const modelToFileMap = {}; // modelName -> fileName
|
|
62
|
+
const tableToFileMap = {}; // tableName -> fileName
|
|
63
|
+
const viewToFileMap = {}; // view name (bare and qualified) -> fileName
|
|
64
|
+
const existingViewNames = {}; // view key -> block name
|
|
65
|
+
if (fs_1.default.existsSync(schemaDir)) {
|
|
66
|
+
const files = fs_1.default.readdirSync(schemaDir).filter(f => f.endsWith(".an5"));
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const fullPath = path_1.default.join(schemaDir, file);
|
|
69
|
+
const text = fs_1.default.readFileSync(fullPath, "utf8");
|
|
70
|
+
fileContents[file] = text;
|
|
71
|
+
// Extract model definitions and table mappings from existing file contents
|
|
72
|
+
const modelRegex = /model\s+(\w+)\s*\{([^}]*)\}/g;
|
|
73
|
+
let match;
|
|
74
|
+
while ((match = modelRegex.exec(text)) !== null) {
|
|
75
|
+
const modelName = match[1];
|
|
76
|
+
const block = match[2];
|
|
77
|
+
modelToFileMap[modelName] = file;
|
|
78
|
+
// Try to find @@map
|
|
79
|
+
const mapMatch = block.match(/@@map\("(.+)"\)/);
|
|
80
|
+
if (mapMatch) {
|
|
81
|
+
const tableName = mapMatch[1];
|
|
82
|
+
tableToFileMap[tableName] = file;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Default pluralization mapping
|
|
86
|
+
const defaultTableName = modelName.toLowerCase() + "s";
|
|
87
|
+
tableToFileMap[defaultTableName] = file;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Views are tracked separately: a model and a view may share a name, and a
|
|
91
|
+
// view without @@map is identified by its block name.
|
|
92
|
+
const viewHeaderRegex = /view\s+(\w+)\s*\{/g;
|
|
93
|
+
while ((match = viewHeaderRegex.exec(text)) !== null) {
|
|
94
|
+
const blockName = match[1];
|
|
95
|
+
const range = (0, pull_schema_1.blockRange)(text, "view", blockName);
|
|
96
|
+
const block = range ? text.slice(range.start, range.end) : "";
|
|
97
|
+
const mapMatch = block.match(/@@map\("(.+)"\)/);
|
|
98
|
+
const objectName = mapMatch ? mapMatch[1] : blockName.toLowerCase() + "s";
|
|
99
|
+
const schemaMatch = block.match(/@@schema\("(.*)"\)/);
|
|
100
|
+
for (const key of new Set([
|
|
101
|
+
blockName,
|
|
102
|
+
objectName,
|
|
103
|
+
(0, pull_schema_1.qualifiedBlockKey)(blockName, schemaMatch?.[1]),
|
|
104
|
+
(0, pull_schema_1.qualifiedBlockKey)(objectName, schemaMatch?.[1]),
|
|
105
|
+
])) {
|
|
106
|
+
viewToFileMap[key] = file;
|
|
107
|
+
existingViewNames[key] = blockName;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
fs_1.default.mkdirSync(schemaDir, { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
// Ensure default core.an5 exists in fileContents
|
|
116
|
+
const defaultFile = "core.an5";
|
|
117
|
+
if (!fileContents[defaultFile]) {
|
|
118
|
+
fileContents[defaultFile] = "";
|
|
119
|
+
}
|
|
120
|
+
const existingModelNames = new Set(Object.keys(modelToFileMap));
|
|
121
|
+
const generatedModels = {};
|
|
122
|
+
for (const table of tables) {
|
|
123
|
+
const { tableName, schemaName } = table;
|
|
124
|
+
console.log(`Introspecting table [${tableName}]...`);
|
|
125
|
+
// Get columns
|
|
126
|
+
const columns = await (await getDb()).$queryRawUnsafe(`
|
|
127
|
+
SELECT
|
|
128
|
+
c.name AS columnName,
|
|
129
|
+
ty.name AS dataType,
|
|
130
|
+
c.max_length AS maxLength,
|
|
131
|
+
c.precision AS precision,
|
|
132
|
+
c.scale AS scale,
|
|
133
|
+
c.is_nullable AS isNullable,
|
|
134
|
+
c.is_identity AS isIdentity,
|
|
135
|
+
pk.is_primary_key AS isPrimaryKey,
|
|
136
|
+
d.definition AS defaultValue
|
|
137
|
+
FROM sys.columns c
|
|
138
|
+
JOIN sys.types ty ON c.user_type_id = ty.user_type_id
|
|
139
|
+
LEFT JOIN (
|
|
140
|
+
SELECT ic.object_id, ic.column_id, i.is_primary_key
|
|
141
|
+
FROM sys.index_columns ic
|
|
142
|
+
JOIN sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id
|
|
143
|
+
WHERE i.is_primary_key = 1
|
|
144
|
+
) pk ON c.object_id = pk.object_id AND c.column_id = pk.column_id
|
|
145
|
+
LEFT JOIN sys.default_constraints d ON c.default_object_id = d.object_id
|
|
146
|
+
WHERE c.object_id = OBJECT_ID(@p_0)
|
|
147
|
+
ORDER BY c.column_id
|
|
148
|
+
`, tableName);
|
|
149
|
+
// Get indexes
|
|
150
|
+
const indexes = await (await getDb()).$queryRawUnsafe(`
|
|
151
|
+
SELECT
|
|
152
|
+
i.name AS indexName,
|
|
153
|
+
i.is_unique AS isUnique,
|
|
154
|
+
i.is_primary_key AS isPrimaryKey,
|
|
155
|
+
c.name AS columnName,
|
|
156
|
+
ic.key_ordinal AS keyOrdinal
|
|
157
|
+
FROM sys.indexes i
|
|
158
|
+
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
|
159
|
+
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
|
160
|
+
WHERE i.object_id = OBJECT_ID(@p_0) AND i.is_hypothetical = 0 AND i.name IS NOT NULL
|
|
161
|
+
ORDER BY i.name, ic.key_ordinal
|
|
162
|
+
`, tableName);
|
|
163
|
+
const indexGroups = {};
|
|
164
|
+
for (const idx of indexes) {
|
|
165
|
+
if (!indexGroups[idx.indexName]) {
|
|
166
|
+
indexGroups[idx.indexName] = {
|
|
167
|
+
isUnique: idx.isUnique === 1 || idx.isUnique === true,
|
|
168
|
+
isPrimaryKey: idx.isPrimaryKey === 1 || idx.isPrimaryKey === true,
|
|
169
|
+
columns: []
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
indexGroups[idx.indexName].columns.push(idx.columnName);
|
|
173
|
+
}
|
|
174
|
+
const uniqueColumns = new Set();
|
|
175
|
+
const compoundUniques = [];
|
|
176
|
+
const modelIndexes = [];
|
|
177
|
+
for (const [name, info] of Object.entries(indexGroups)) {
|
|
178
|
+
if (info.isPrimaryKey)
|
|
179
|
+
continue;
|
|
180
|
+
if (info.isUnique) {
|
|
181
|
+
if (info.columns.length === 1) {
|
|
182
|
+
uniqueColumns.add(info.columns[0]);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
compoundUniques.push(info.columns);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
modelIndexes.push(info.columns);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let modelName = "";
|
|
193
|
+
for (const [mName, file] of Object.entries(modelToFileMap)) {
|
|
194
|
+
const text = fileContents[file];
|
|
195
|
+
const modelRegex = new RegExp(`model\\s+(${mName})\\s*\\{[^}]*\\}`, 'g');
|
|
196
|
+
let m;
|
|
197
|
+
while ((m = modelRegex.exec(text)) !== null) {
|
|
198
|
+
const block = m[0];
|
|
199
|
+
const mapMatch = block.match(/@@map\("(.+)"\)/);
|
|
200
|
+
if (mapMatch && mapMatch[1] === tableName) {
|
|
201
|
+
modelName = mName;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (modelName)
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
if (!modelName) {
|
|
209
|
+
// A schema pulled before `blockNameForObject` learned to split snake_case
|
|
210
|
+
// has a block named `Order_item`; keep matching it rather than adding a
|
|
211
|
+
// second block for the same table.
|
|
212
|
+
const legacy = legacyBlockName(tableName);
|
|
213
|
+
modelName = modelToFileMap[legacy] ? legacy : (0, pull_schema_1.blockNameForObject)(tableName);
|
|
214
|
+
}
|
|
215
|
+
// Parse existing model fields from file for merging
|
|
216
|
+
const existingFields = {};
|
|
217
|
+
const targetFile = modelToFileMap[modelName] || defaultFile;
|
|
218
|
+
const fileContent = fileContents[targetFile];
|
|
219
|
+
if (fileContent) {
|
|
220
|
+
const modelRegex = new RegExp(`model\\s+${modelName}\\s*\\{([^}]*)\\}`, 'g');
|
|
221
|
+
const match = modelRegex.exec(fileContent);
|
|
222
|
+
if (match) {
|
|
223
|
+
const block = match[1];
|
|
224
|
+
const lines = block.split("\n");
|
|
225
|
+
for (let line of lines) {
|
|
226
|
+
line = line.trim();
|
|
227
|
+
if (!line || line.startsWith("@@") || line.startsWith("model ") || line === "}")
|
|
228
|
+
continue;
|
|
229
|
+
const parts = line.split(/\s+/);
|
|
230
|
+
const name = parts[0];
|
|
231
|
+
const type = parts[1];
|
|
232
|
+
if (!name || !type)
|
|
233
|
+
continue;
|
|
234
|
+
const cleanType = type.replace("[]", "").replace("?", "");
|
|
235
|
+
const isRelation = existingModelNames.has(cleanType) || type.endsWith("[]");
|
|
236
|
+
existingFields[name.toLowerCase()] = {
|
|
237
|
+
line,
|
|
238
|
+
isRelation,
|
|
239
|
+
name
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
let modelStr = `model ${modelName} {\n`;
|
|
245
|
+
for (const col of columns) {
|
|
246
|
+
const { columnName, dataType, isNullable, isPrimaryKey, isIdentity, defaultValue } = col;
|
|
247
|
+
const sqlType = (0, pull_schema_1.sqlServerTypeForColumn)(col);
|
|
248
|
+
const dt = dataType.toLowerCase();
|
|
249
|
+
const optional = isNullable ? "?" : "";
|
|
250
|
+
let attributes = "";
|
|
251
|
+
if (isPrimaryKey)
|
|
252
|
+
attributes += " @id";
|
|
253
|
+
if (isIdentity)
|
|
254
|
+
attributes += " @default(autoincrement())";
|
|
255
|
+
if (uniqueColumns.has(columnName) && !isPrimaryKey)
|
|
256
|
+
attributes += " @unique";
|
|
257
|
+
if (defaultValue) {
|
|
258
|
+
if (defaultValue.includes("newid()")) {
|
|
259
|
+
attributes += " @default(uuid())";
|
|
260
|
+
}
|
|
261
|
+
else if (defaultValue.includes("getdate()") || defaultValue.includes("sysdatetime()")) {
|
|
262
|
+
attributes += " @default(now())";
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const cleaned = defaultValue.replace(/^\(+(.*?)\)+$/, '$1');
|
|
266
|
+
if (dt === "bit") {
|
|
267
|
+
if (cleaned === "1" || cleaned.toLowerCase() === "true")
|
|
268
|
+
attributes += " @default(true)";
|
|
269
|
+
else if (cleaned === "0" || cleaned.toLowerCase() === "false")
|
|
270
|
+
attributes += " @default(false)";
|
|
271
|
+
}
|
|
272
|
+
else if (dt === "int" || dt === "float" || dt === "bigint" || dt === "decimal" || dt === "numeric") {
|
|
273
|
+
if (!isNaN(Number(cleaned)))
|
|
274
|
+
attributes += ` @default(${cleaned})`;
|
|
275
|
+
}
|
|
276
|
+
else if (dt === "nvarchar" || dt === "varchar") {
|
|
277
|
+
if (cleaned.startsWith("'") && cleaned.endsWith("'")) {
|
|
278
|
+
const val = cleaned.slice(1, -1).replace(/"/g, '\\"');
|
|
279
|
+
attributes += ` @default("${val}")`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
let fieldLine = `${columnName} ${sqlType}${optional}${attributes}`;
|
|
285
|
+
// Merge decorators/attributes from existing schema
|
|
286
|
+
const existing = existingFields[columnName.toLowerCase()];
|
|
287
|
+
if (existing) {
|
|
288
|
+
const attributesMatch = existing.line.match(/(@default\(.*?\)|@updatedAt)/g);
|
|
289
|
+
if (attributesMatch) {
|
|
290
|
+
for (const attr of attributesMatch) {
|
|
291
|
+
if (!fieldLine.includes(attr)) {
|
|
292
|
+
if (attr.startsWith("@default") && fieldLine.includes("@default")) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
fieldLine += ` ${attr}`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
modelStr += ` ${fieldLine}\n`;
|
|
301
|
+
}
|
|
302
|
+
// Append preserved relations
|
|
303
|
+
for (const [lowerName, fieldInfo] of Object.entries(existingFields)) {
|
|
304
|
+
if (fieldInfo.isRelation) {
|
|
305
|
+
modelStr += ` ${fieldInfo.line}\n`;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const uniqueStrings = new Set();
|
|
309
|
+
for (const cols of compoundUniques) {
|
|
310
|
+
uniqueStrings.add(`@@unique([${cols.join(", ")}])`);
|
|
311
|
+
}
|
|
312
|
+
for (const key of uniqueStrings) {
|
|
313
|
+
modelStr += ` ${key}\n`;
|
|
314
|
+
}
|
|
315
|
+
const indexStrings = new Set();
|
|
316
|
+
for (const cols of modelIndexes) {
|
|
317
|
+
indexStrings.add(`@@index([${cols.join(", ")}])`);
|
|
318
|
+
}
|
|
319
|
+
for (const key of indexStrings) {
|
|
320
|
+
modelStr += ` ${key}\n`;
|
|
321
|
+
}
|
|
322
|
+
if (tableName !== modelName.toLowerCase() + "s") {
|
|
323
|
+
modelStr += `\n @@map("${tableName}")\n`;
|
|
324
|
+
}
|
|
325
|
+
// A table outside `dbo` is only the same object when the schema travels with
|
|
326
|
+
// it; without this, pushing the pulled schema created it in the connection's
|
|
327
|
+
// default schema.
|
|
328
|
+
if (schemaName && schemaName.toLowerCase() !== "dbo") {
|
|
329
|
+
modelStr += `${tableName === modelName.toLowerCase() + "s" ? "\n" : ""} @@schema("${schemaName}")\n`;
|
|
330
|
+
}
|
|
331
|
+
modelStr += "}";
|
|
332
|
+
generatedModels[modelName] = modelStr;
|
|
333
|
+
}
|
|
334
|
+
// Views are pulled after the tables, for the same reason push creates them
|
|
335
|
+
// after the tables: they may read a table this schema declares.
|
|
336
|
+
const views = await (await getDb()).$queryRawUnsafe(`
|
|
337
|
+
SELECT s.name AS schemaName, v.name AS viewName, OBJECT_DEFINITION(v.object_id) AS definition
|
|
338
|
+
FROM sys.views v
|
|
339
|
+
JOIN sys.schemas s ON v.schema_id = s.schema_id
|
|
340
|
+
WHERE v.is_ms_shipped = 0
|
|
341
|
+
ORDER BY s.name, v.name
|
|
342
|
+
`);
|
|
343
|
+
const generatedViews = {};
|
|
344
|
+
for (const view of views) {
|
|
345
|
+
const { viewName, schemaName, definition } = view;
|
|
346
|
+
const key = (0, pull_schema_1.qualifiedBlockKey)(viewName, schemaName);
|
|
347
|
+
// The name a previous pull would have chosen, kept so the block is updated
|
|
348
|
+
// in place instead of a duplicate appearing beside it.
|
|
349
|
+
const legacy = legacyBlockName(viewName);
|
|
350
|
+
const modelName = existingViewNames[key] ?? existingViewNames[viewName]
|
|
351
|
+
?? (existingViewNames[legacy] ? legacy : (0, pull_schema_1.blockNameForObject)(viewName));
|
|
352
|
+
const targetFile = viewToFileMap[modelName] ?? viewToFileMap[key] ?? viewToFileMap[viewName] ?? defaultFile;
|
|
353
|
+
console.log(`Introspecting view [${key}]...`);
|
|
354
|
+
const columns = await (await getDb()).$queryRawUnsafe(`
|
|
355
|
+
SELECT
|
|
356
|
+
c.name AS columnName,
|
|
357
|
+
ty.name AS dataType,
|
|
358
|
+
c.max_length AS maxLength,
|
|
359
|
+
c.precision AS precision,
|
|
360
|
+
c.scale AS scale,
|
|
361
|
+
c.is_nullable AS isNullable
|
|
362
|
+
FROM sys.columns c
|
|
363
|
+
JOIN sys.types ty ON c.user_type_id = ty.user_type_id
|
|
364
|
+
WHERE c.object_id = OBJECT_ID(@p_0)
|
|
365
|
+
ORDER BY c.column_id
|
|
366
|
+
`, key);
|
|
367
|
+
const block = (0, pull_schema_1.buildViewBlock)({ modelName, viewName, schemaName, columns, definition });
|
|
368
|
+
if (block === null) {
|
|
369
|
+
// Without a definition there is nothing to put in `@@sql`, and a view block
|
|
370
|
+
// without one is rejected by push and migrate. Keep the database object and
|
|
371
|
+
// tell the user why it is the only one not in the schema.
|
|
372
|
+
console.warn(`⚠️ Skipped view [${key}]: its definition could not be read (VIEW DEFINITION permission?).`);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
generatedViews[modelName] = { block, file: targetFile };
|
|
376
|
+
}
|
|
377
|
+
for (const [modelName, modelStr] of Object.entries(generatedModels)) {
|
|
378
|
+
const targetFile = modelToFileMap[modelName] || defaultFile;
|
|
379
|
+
const fileContent = fileContents[targetFile];
|
|
380
|
+
const modelRegex = new RegExp(`model\\s+${modelName}\\s*\\{[^}]*\\}`, 'g');
|
|
381
|
+
if (modelRegex.test(fileContent)) {
|
|
382
|
+
fileContents[targetFile] = fileContent.replace(modelRegex, modelStr);
|
|
383
|
+
console.log(`✏️ Updated model [${modelName}] in file [${targetFile}]`);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
fileContents[targetFile] = fileContent.trim() + "\n\n" + modelStr + "\n";
|
|
387
|
+
console.log(`➕ Added model [${modelName}] to file [${targetFile}]`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (const [viewName, entry] of Object.entries(generatedViews)) {
|
|
391
|
+
const fileContent = fileContents[entry.file];
|
|
392
|
+
const range = (0, pull_schema_1.blockRange)(fileContent, "view", viewName);
|
|
393
|
+
if (range) {
|
|
394
|
+
fileContents[entry.file] = fileContent.slice(0, range.start) + entry.block + fileContent.slice(range.end);
|
|
395
|
+
console.log(`✏️ Updated view [${viewName}] in file [${entry.file}]`);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
fileContents[entry.file] = fileContent.trim() + "\n\n" + entry.block + "\n";
|
|
399
|
+
console.log(`➕ Added view [${viewName}] to file [${entry.file}]`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
for (const [file, content] of Object.entries(fileContents)) {
|
|
403
|
+
const fullPath = path_1.default.join(schemaDir, file);
|
|
404
|
+
fs_1.default.writeFileSync(fullPath, content.trim() + "\n");
|
|
405
|
+
console.log(`💾 Saved updates to [${fullPath}]`);
|
|
406
|
+
}
|
|
407
|
+
// Index schema into vector store for RAG
|
|
408
|
+
console.log("\n📚 Indexing schema into vector store for RAG...");
|
|
409
|
+
try {
|
|
410
|
+
const { indexSchema, indexQuerySamples } = require(path_1.default.join(rootDir, "an5Agent", "dist", "rag", "indexer.js"));
|
|
411
|
+
await indexSchema(schemaDir);
|
|
412
|
+
await indexQuerySamples();
|
|
413
|
+
console.log("✅ Schema indexed for RAG.");
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
console.warn(`⚠️ RAG indexing skipped: ${err.message || err}`);
|
|
417
|
+
console.warn(` Run 'npm --prefix an5Agent run rag:index' manually to build vector store.`);
|
|
418
|
+
}
|
|
419
|
+
console.log("✅ Database schema pulling completed successfully.");
|
|
420
|
+
process.exit(0);
|
|
421
|
+
}
|
|
422
|
+
pull().catch((err) => {
|
|
423
|
+
console.error("❌ Pull failed:", err);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
});
|
package/dist/push.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "dotenv/config";
|
package/dist/push.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
require("dotenv/config");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const adapters_1 = require("@an5/adapters");
|
|
10
|
+
const config_1 = require("./generator/src/config");
|
|
11
|
+
const field_types_1 = require("./generator/src/field-types");
|
|
12
|
+
const dialect_1 = require("./generator/src/dialect");
|
|
13
|
+
const push_apply_1 = require("./generator/src/push-apply");
|
|
14
|
+
const push_schema_1 = require("./generator/src/push-schema");
|
|
15
|
+
const rootDir = process.cwd();
|
|
16
|
+
const { config, outputs } = (0, config_1.loadConfig)();
|
|
17
|
+
const schemaDir = outputs.schemaDir;
|
|
18
|
+
// The target provider comes from the connection string: the allowed types have
|
|
19
|
+
// to match the database about to receive the SQL, not one list shared by every
|
|
20
|
+
// database.
|
|
21
|
+
const provider = (0, config_1.providerFromConfig)(config, process.env);
|
|
22
|
+
let _adapter = null;
|
|
23
|
+
async function getDb() {
|
|
24
|
+
if (!_adapter) {
|
|
25
|
+
_adapter = new adapters_1.An5Adapter({ connectionString: (0, config_1.resolveConnectionString)(config, process.env, "db:push") });
|
|
26
|
+
await _adapter.$connect();
|
|
27
|
+
}
|
|
28
|
+
return _adapter;
|
|
29
|
+
}
|
|
30
|
+
async function push() {
|
|
31
|
+
const dialect = (0, dialect_1.dialectFor)(provider);
|
|
32
|
+
// Google Sheets is a grid of cells, not tables: there is no DDL to send and
|
|
33
|
+
// nothing to create. Say that instead of failing inside the driver.
|
|
34
|
+
if (!dialect.supportsDdl) {
|
|
35
|
+
console.error(`❌ db:push has nothing to create for ${field_types_1.PROVIDER_LABELS[provider]}: a sheet is a\n` +
|
|
36
|
+
' range of cells, not a table, so there is no DDL to send.\n' +
|
|
37
|
+
' Write rows through the adapter instead — the header row appears on the\n' +
|
|
38
|
+
' first insert.');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
let schemaText = "";
|
|
42
|
+
if (fs_1.default.existsSync(schemaDir)) {
|
|
43
|
+
const files = fs_1.default.readdirSync(schemaDir).filter(f => f.endsWith(".an5"));
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
schemaText += fs_1.default.readFileSync(path_1.default.join(schemaDir, file), "utf8") + "\n";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const schemaPath = path_1.default.join(__dirname, "schema.an5");
|
|
50
|
+
if (fs_1.default.existsSync(schemaPath)) {
|
|
51
|
+
schemaText = fs_1.default.readFileSync(schemaPath, "utf8");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.error(`No schema found in ${schemaDir} or schema.an5`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const { models, views, issues } = (0, push_schema_1.parsePushSchema)(schemaText, provider);
|
|
59
|
+
if (issues.length > 0)
|
|
60
|
+
throw new field_types_1.FieldTypeError(provider, issues);
|
|
61
|
+
console.log(`🚀 Pushing schema to database (${field_types_1.PROVIDER_LABELS[provider]})...`);
|
|
62
|
+
const result = await (0, push_apply_1.applySchema)(models, dialect, {
|
|
63
|
+
query: async (sql) => (await getDb()).$queryRawUnsafe(sql),
|
|
64
|
+
execute: async (sql) => {
|
|
65
|
+
await (await getDb()).$executeRawUnsafe(sql);
|
|
66
|
+
},
|
|
67
|
+
log: (message) => console.log(message),
|
|
68
|
+
}, views);
|
|
69
|
+
if (result.tablesCreated > 0)
|
|
70
|
+
console.log(` ${result.tablesCreated} table(s) created.`);
|
|
71
|
+
if (result.columnsAdded > 0)
|
|
72
|
+
console.log(` ${result.columnsAdded} column(s) added.`);
|
|
73
|
+
if (result.uniqueConstraintsAdded > 0)
|
|
74
|
+
console.log(` ${result.uniqueConstraintsAdded} unique(s) added.`);
|
|
75
|
+
if (result.indexesCreated > 0)
|
|
76
|
+
console.log(` ${result.indexesCreated} index(es) created.`);
|
|
77
|
+
if (result.viewsApplied > 0)
|
|
78
|
+
console.log(` ${result.viewsApplied} view(s) created or replaced.`);
|
|
79
|
+
console.log("✅ Database push completed.");
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
push().catch((err) => {
|
|
83
|
+
// A field type error carries one line per offending field; the message alone
|
|
84
|
+
// would name the provider without saying which fields to fix.
|
|
85
|
+
if (err instanceof field_types_1.FieldTypeError) {
|
|
86
|
+
console.error(`❌ ${err.message}:`);
|
|
87
|
+
console.error((0, config_1.formatIssues)(err.issues));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.error(`❌ Push failed: ${err instanceof Error ? err.message : err}`);
|
|
91
|
+
}
|
|
92
|
+
process.exit(1);
|
|
93
|
+
});
|
package/dist/seed.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "dotenv/config";
|
package/dist/seed.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
require("dotenv/config");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
async function main() {
|
|
10
|
+
const rootDir = process.cwd();
|
|
11
|
+
const candidates = [
|
|
12
|
+
path_1.default.join(rootDir, "seed.ts"),
|
|
13
|
+
path_1.default.join(rootDir, "seed.js"),
|
|
14
|
+
path_1.default.join(rootDir, "an5", "seed.ts"),
|
|
15
|
+
path_1.default.join(rootDir, "an5", "seed.js"),
|
|
16
|
+
path_1.default.join(rootDir, "scripts", "seed.ts"),
|
|
17
|
+
path_1.default.join(rootDir, "scripts", "seed.js"),
|
|
18
|
+
];
|
|
19
|
+
for (const seedPath of candidates) {
|
|
20
|
+
if (fs_1.default.existsSync(seedPath)) {
|
|
21
|
+
const relPath = path_1.default.relative(rootDir, seedPath);
|
|
22
|
+
console.log(`🌱 Executing project seed script: ${relPath}...`);
|
|
23
|
+
require(seedPath);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
console.log("⚠️ No project seed script found.");
|
|
28
|
+
console.log("👉 Create a seed.ts or seed.js file in your project root or an5/ directory to seed your database.");
|
|
29
|
+
}
|
|
30
|
+
main().catch((err) => {
|
|
31
|
+
console.error("❌ Seed execution failed:", err);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|