forge-admin 0.0.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.
- package/README.md +73 -0
- package/app.db +0 -0
- package/components.json +20 -0
- package/dist/assets/index-BPVmexx_.css +1 -0
- package/dist/assets/index-BtNewH3n.js +258 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.html +27 -0
- package/dist/placeholder.svg +1 -0
- package/dist/robots.txt +14 -0
- package/eslint.config.js +26 -0
- package/index.html +26 -0
- package/package.json +107 -0
- package/postcss.config.js +6 -0
- package/public/favicon.ico +0 -0
- package/public/placeholder.svg +1 -0
- package/public/robots.txt +14 -0
- package/src/App.css +42 -0
- package/src/App.tsx +32 -0
- package/src/admin/convertSchema.ts +83 -0
- package/src/admin/factory.ts +12 -0
- package/src/admin/introspecter.ts +6 -0
- package/src/admin/router.ts +38 -0
- package/src/admin/schema.ts +17 -0
- package/src/admin/sqlite.ts +73 -0
- package/src/admin/types.ts +35 -0
- package/src/components/AdminLayout.tsx +19 -0
- package/src/components/AdminSidebar.tsx +102 -0
- package/src/components/DataTable.tsx +166 -0
- package/src/components/ModelForm.tsx +221 -0
- package/src/components/NavLink.tsx +28 -0
- package/src/components/StatCard.tsx +32 -0
- package/src/components/ui/accordion.tsx +52 -0
- package/src/components/ui/alert-dialog.tsx +104 -0
- package/src/components/ui/alert.tsx +43 -0
- package/src/components/ui/aspect-ratio.tsx +5 -0
- package/src/components/ui/avatar.tsx +38 -0
- package/src/components/ui/badge.tsx +29 -0
- package/src/components/ui/breadcrumb.tsx +90 -0
- package/src/components/ui/button.tsx +47 -0
- package/src/components/ui/calendar.tsx +54 -0
- package/src/components/ui/card.tsx +43 -0
- package/src/components/ui/carousel.tsx +224 -0
- package/src/components/ui/chart.tsx +303 -0
- package/src/components/ui/checkbox.tsx +26 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/command.tsx +132 -0
- package/src/components/ui/context-menu.tsx +178 -0
- package/src/components/ui/dialog.tsx +95 -0
- package/src/components/ui/drawer.tsx +87 -0
- package/src/components/ui/dropdown-menu.tsx +179 -0
- package/src/components/ui/form.tsx +129 -0
- package/src/components/ui/hover-card.tsx +27 -0
- package/src/components/ui/input-otp.tsx +61 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +17 -0
- package/src/components/ui/menubar.tsx +207 -0
- package/src/components/ui/navigation-menu.tsx +120 -0
- package/src/components/ui/pagination.tsx +81 -0
- package/src/components/ui/popover.tsx +29 -0
- package/src/components/ui/progress.tsx +23 -0
- package/src/components/ui/radio-group.tsx +36 -0
- package/src/components/ui/resizable.tsx +37 -0
- package/src/components/ui/scroll-area.tsx +38 -0
- package/src/components/ui/select.tsx +143 -0
- package/src/components/ui/separator.tsx +20 -0
- package/src/components/ui/sheet.tsx +107 -0
- package/src/components/ui/sidebar.tsx +637 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/slider.tsx +23 -0
- package/src/components/ui/sonner.tsx +27 -0
- package/src/components/ui/switch.tsx +27 -0
- package/src/components/ui/table.tsx +72 -0
- package/src/components/ui/tabs.tsx +53 -0
- package/src/components/ui/textarea.tsx +21 -0
- package/src/components/ui/toast.tsx +111 -0
- package/src/components/ui/toaster.tsx +24 -0
- package/src/components/ui/toggle-group.tsx +49 -0
- package/src/components/ui/toggle.tsx +37 -0
- package/src/components/ui/tooltip.tsx +28 -0
- package/src/components/ui/use-toast.ts +3 -0
- package/src/config/define.ts +6 -0
- package/src/config/index.ts +0 -0
- package/src/config/load.ts +45 -0
- package/src/config/types.ts +5 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/hooks/use-toast.ts +186 -0
- package/src/index.css +142 -0
- package/src/lib/models.ts +138 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +5 -0
- package/src/orm/cli/makemigrations.ts +63 -0
- package/src/orm/cli/migrate.ts +127 -0
- package/src/orm/cli.ts +30 -0
- package/src/orm/core/base-model.ts +6 -0
- package/src/orm/core/manager.ts +27 -0
- package/src/orm/core/query-builder.ts +74 -0
- package/src/orm/db/connection.ts +0 -0
- package/src/orm/db/sql-types.ts +72 -0
- package/src/orm/db/sqlite.ts +4 -0
- package/src/orm/decorators/field.ts +80 -0
- package/src/orm/decorators/model.ts +36 -0
- package/src/orm/decorators/relations.ts +0 -0
- package/src/orm/metadata/field-metadata.ts +0 -0
- package/src/orm/metadata/field-types.ts +12 -0
- package/src/orm/metadata/get-meta.ts +9 -0
- package/src/orm/metadata/index.ts +15 -0
- package/src/orm/metadata/keys.ts +2 -0
- package/src/orm/metadata/model-registry.ts +53 -0
- package/src/orm/metadata/modifiers.ts +26 -0
- package/src/orm/metadata/types.ts +45 -0
- package/src/orm/migration-engine/diff.ts +243 -0
- package/src/orm/migration-engine/operations.ts +186 -0
- package/src/orm/schema/build.ts +138 -0
- package/src/orm/schema/state.ts +23 -0
- package/src/orm/schema/writeMigrations.ts +21 -0
- package/src/orm/syncdb.ts +25 -0
- package/src/pages/Dashboard.tsx +127 -0
- package/src/pages/Index.tsx +18 -0
- package/src/pages/ModelPage.tsx +177 -0
- package/src/pages/NotFound.tsx +24 -0
- package/src/pages/SchemaEditor.tsx +170 -0
- package/src/pages/Settings.tsx +166 -0
- package/src/server.ts +69 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +112 -0
- package/tailwind.config.ts +114 -0
- package/tsconfig.app.json +30 -0
- package/tsconfig.json +16 -0
- package/tsconfig.node.json +22 -0
- package/vite.config.js +23 -0
- package/vite.config.ts +18 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
|
|
2
|
+
import { db } from "../db/sqlite";
|
|
3
|
+
|
|
4
|
+
function sameIndex(a: any, b: any) {
|
|
5
|
+
return (
|
|
6
|
+
a.unique === b.unique &&
|
|
7
|
+
a.fields.length === b.fields.length &&
|
|
8
|
+
a.fields.every((f: string, i: number) => f === b.fields[i])
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
function getRowCount(table: string): number {
|
|
14
|
+
const row = db
|
|
15
|
+
.prepare(`SELECT COUNT(*) as count FROM "${table}"`)
|
|
16
|
+
.get();
|
|
17
|
+
|
|
18
|
+
return row?.count ?? 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validateAddColumn(
|
|
22
|
+
op: any,
|
|
23
|
+
existingRowCount: number
|
|
24
|
+
) {
|
|
25
|
+
const f = op.fieldMeta;
|
|
26
|
+
|
|
27
|
+
if (!f.options?.nullable && f.options?.default == null && existingRowCount > 0) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Cannot add non-nullable field '${f.name}' without a default. ` +
|
|
30
|
+
`Forge requires either a default or nullable=true.`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
function sameFields(a: any, b: any) {
|
|
38
|
+
const aKeys = Object.keys(a);
|
|
39
|
+
const bKeys = Object.keys(b);
|
|
40
|
+
|
|
41
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
42
|
+
|
|
43
|
+
return aKeys.every(key =>
|
|
44
|
+
b[key] &&
|
|
45
|
+
a[key].type === b[key].type &&
|
|
46
|
+
JSON.stringify(a[key].options ?? {}) ===
|
|
47
|
+
JSON.stringify(b[key].options ?? {})
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function diffStates(oldState: any, newState: any) {
|
|
52
|
+
const ops: any[] = [];
|
|
53
|
+
|
|
54
|
+
const oldModels = oldState.models;
|
|
55
|
+
const newModels = newState.models;
|
|
56
|
+
|
|
57
|
+
const oldTables = Object.keys(oldModels);
|
|
58
|
+
const newTables = Object.keys(newModels);
|
|
59
|
+
|
|
60
|
+
const removedTables = oldTables.filter(t => !newTables.includes(t));
|
|
61
|
+
const addedTables = newTables.filter(t => !oldTables.includes(t));
|
|
62
|
+
|
|
63
|
+
const consumedOldTables = new Set<string>();
|
|
64
|
+
const consumedNewTables = new Set<string>();
|
|
65
|
+
|
|
66
|
+
// 1️⃣ TABLE RENAME DETECTION
|
|
67
|
+
for (const oldTable of removedTables) {
|
|
68
|
+
for (const newTable of addedTables) {
|
|
69
|
+
if (consumedOldTables.has(oldTable)) continue;
|
|
70
|
+
if (consumedNewTables.has(newTable)) continue;
|
|
71
|
+
|
|
72
|
+
const a = oldModels[oldTable];
|
|
73
|
+
const b = newModels[newTable];
|
|
74
|
+
|
|
75
|
+
if (sameFields(a.fields, b.fields)) {
|
|
76
|
+
ops.push({
|
|
77
|
+
type: "RenameTable",
|
|
78
|
+
from: oldTable,
|
|
79
|
+
to: newTable,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
consumedOldTables.add(oldTable);
|
|
83
|
+
consumedNewTables.add(newTable);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2️⃣ CREATE TABLE
|
|
89
|
+
for (const table of newTables) {
|
|
90
|
+
if (oldModels[table]) continue;
|
|
91
|
+
if (consumedNewTables.has(table)) continue;
|
|
92
|
+
|
|
93
|
+
ops.push({
|
|
94
|
+
type: "CreateTable",
|
|
95
|
+
meta: newModels[table],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 3️⃣ DROP TABLE
|
|
100
|
+
for (const table of oldTables) {
|
|
101
|
+
if (newModels[table]) continue;
|
|
102
|
+
if (consumedOldTables.has(table)) continue;
|
|
103
|
+
|
|
104
|
+
ops.push({
|
|
105
|
+
type: "DropTable",
|
|
106
|
+
model: table,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 4️⃣ COLUMN-LEVEL DIFFS
|
|
111
|
+
for (const table of newTables) {
|
|
112
|
+
if (!oldModels[table]) continue;
|
|
113
|
+
if (consumedNewTables.has(table)) continue;
|
|
114
|
+
|
|
115
|
+
const oldFields = oldModels[table].fields;
|
|
116
|
+
const newFields = newModels[table].fields;
|
|
117
|
+
|
|
118
|
+
const removedFields = Object.keys(oldFields).filter(
|
|
119
|
+
f => !newFields[f]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const addedFields = Object.keys(newFields).filter(
|
|
123
|
+
f => !oldFields[f]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const consumedOldFields = new Set<string>();
|
|
127
|
+
const consumedNewFields = new Set<string>();
|
|
128
|
+
|
|
129
|
+
// 4️⃣a COLUMN RENAME DETECTION
|
|
130
|
+
for (const oldField of removedFields) {
|
|
131
|
+
for (const newField of addedFields) {
|
|
132
|
+
if (consumedOldFields.has(oldField)) continue;
|
|
133
|
+
if (consumedNewFields.has(newField)) continue;
|
|
134
|
+
|
|
135
|
+
const a = oldFields[oldField];
|
|
136
|
+
const b = newFields[newField];
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
a.type === b.type &&
|
|
140
|
+
JSON.stringify(a.options ?? {}) ===
|
|
141
|
+
JSON.stringify(b.options ?? {})
|
|
142
|
+
) {
|
|
143
|
+
ops.push({
|
|
144
|
+
type: "RenameColumn",
|
|
145
|
+
model: table,
|
|
146
|
+
from: oldField,
|
|
147
|
+
to: newField,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
consumedOldFields.add(oldField);
|
|
151
|
+
consumedNewFields.add(newField);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 4️⃣b ADD COLUMN
|
|
157
|
+
for (const field of addedFields) {
|
|
158
|
+
if (consumedNewFields.has(field)) continue;
|
|
159
|
+
|
|
160
|
+
validateAddColumn(
|
|
161
|
+
{
|
|
162
|
+
type: "AddColumn",
|
|
163
|
+
model: table,
|
|
164
|
+
field: field,
|
|
165
|
+
fieldMeta: newFields[field],
|
|
166
|
+
},
|
|
167
|
+
getRowCount(table)
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
ops.push({
|
|
171
|
+
type: "AddColumn",
|
|
172
|
+
model: table,
|
|
173
|
+
field,
|
|
174
|
+
fieldMeta: newFields[field],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 4️⃣c REMOVE COLUMN
|
|
179
|
+
for (const field of removedFields) {
|
|
180
|
+
if (consumedOldFields.has(field)) continue;
|
|
181
|
+
|
|
182
|
+
ops.push({
|
|
183
|
+
type: "RemoveColumn",
|
|
184
|
+
model: table,
|
|
185
|
+
field,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 5️⃣ INDEX DIFFS
|
|
191
|
+
const seenCreate = new Set<string>();
|
|
192
|
+
const seenDelete = new Set<string>();
|
|
193
|
+
for (const table of newTables) {
|
|
194
|
+
if (!oldModels[table]) continue;
|
|
195
|
+
|
|
196
|
+
const oldIndexes = oldModels[table].indexes ?? [];
|
|
197
|
+
const newIndexes = newModels[table].indexes ?? [];
|
|
198
|
+
|
|
199
|
+
const consumedOld = new Set<number>();
|
|
200
|
+
const consumedNew = new Set<number>();
|
|
201
|
+
|
|
202
|
+
// Detect unchanged / renamed indexes (future-proof)
|
|
203
|
+
for (let i = 0; i < oldIndexes.length; i++) {
|
|
204
|
+
for (let j = 0; j < newIndexes.length; j++) {
|
|
205
|
+
if (consumedOld.has(i) || consumedNew.has(j)) continue;
|
|
206
|
+
|
|
207
|
+
if (sameIndex(oldIndexes[i], newIndexes[j])) {
|
|
208
|
+
consumedOld.add(i);
|
|
209
|
+
consumedNew.add(j);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Added indexes
|
|
215
|
+
newIndexes.forEach((idx: any, i: number) => {
|
|
216
|
+
if (consumedNew.has(i)) return;
|
|
217
|
+
if(!seenCreate.has(idx['name'])){
|
|
218
|
+
ops.push({
|
|
219
|
+
type: "CreateIndex",
|
|
220
|
+
table,
|
|
221
|
+
index: idx,
|
|
222
|
+
});
|
|
223
|
+
seenCreate.add(idx['name']);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Removed indexes
|
|
228
|
+
oldIndexes.forEach((idx: any, i: number) => {
|
|
229
|
+
if (consumedOld.has(i)) return;
|
|
230
|
+
if(!seenDelete.has(idx['name'])){
|
|
231
|
+
ops.push({
|
|
232
|
+
type: "DropIndex",
|
|
233
|
+
table,
|
|
234
|
+
index: idx,
|
|
235
|
+
});
|
|
236
|
+
seenDelete.add(idx['name']);
|
|
237
|
+
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return ops;
|
|
243
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { fieldToSQL } from "../db/sql-types";
|
|
2
|
+
import { ModelMeta } from "../metadata/types";
|
|
3
|
+
import { createIndexSQL, dropIndexSQL } from "../db/sql-types";
|
|
4
|
+
import { db } from "../db/sqlite";
|
|
5
|
+
|
|
6
|
+
function createTableSQL(tableName: string, meta: ModelMeta): string {
|
|
7
|
+
const columns: string[] = [];
|
|
8
|
+
const foreignKeys: string[] = [];
|
|
9
|
+
|
|
10
|
+
for (const field of Object.values(meta.fields)) {
|
|
11
|
+
if (field.type === "foreignkey") {
|
|
12
|
+
columns.push(
|
|
13
|
+
`"${field.name}" INTEGER ${field.options?.default ? `DEFAULT ${field.options.default}` : "NOT NULL"}`
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
foreignKeys.push(
|
|
17
|
+
`FOREIGN KEY ("${field.name}") REFERENCES "${field.options.to}"("${field.options.toField}")` +
|
|
18
|
+
(field.options.onDelete
|
|
19
|
+
? ` ON DELETE ${field.options.onDelete}`
|
|
20
|
+
: "")
|
|
21
|
+
);
|
|
22
|
+
} else {
|
|
23
|
+
columns.push(`"${field.name}" ${fieldToSQL(field)}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return `
|
|
28
|
+
CREATE TABLE "${tableName}" (
|
|
29
|
+
${[...columns, ...foreignKeys].join(",\n")}
|
|
30
|
+
);
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
export function rebuildTable(
|
|
36
|
+
table: string,
|
|
37
|
+
oldState: ModelMeta,
|
|
38
|
+
newState: ModelMeta
|
|
39
|
+
): string[] {
|
|
40
|
+
const tempTable = `${table}__new`;
|
|
41
|
+
|
|
42
|
+
const columns = Object.keys(oldState.fields).filter(
|
|
43
|
+
c => newState.fields[c]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const columnList = columns.map(c => `"${c}"`).join(", ");
|
|
47
|
+
|
|
48
|
+
return [
|
|
49
|
+
createTableSQL(tempTable, newState),
|
|
50
|
+
`
|
|
51
|
+
INSERT INTO "${tempTable}" (${columnList})
|
|
52
|
+
SELECT ${columnList} FROM "${table}";
|
|
53
|
+
`,
|
|
54
|
+
`DROP TABLE "${table}";`,
|
|
55
|
+
`ALTER TABLE "${tempTable}" RENAME TO "${table}";`,
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
export function opToSQL(op: any): string | null {
|
|
62
|
+
switch (op.type) {
|
|
63
|
+
case "CreateTable":
|
|
64
|
+
return createTableSQL(op.meta.tableName, op.meta);
|
|
65
|
+
|
|
66
|
+
case "RenameColumn":
|
|
67
|
+
return `
|
|
68
|
+
ALTER TABLE ${op.model}
|
|
69
|
+
RENAME COLUMN ${op.from} TO ${op.to};
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
case "RenameTable":
|
|
73
|
+
return `
|
|
74
|
+
ALTER TABLE "${op.from}"
|
|
75
|
+
RENAME TO "${op.to}";
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
case "AddColumn":
|
|
80
|
+
return `ALTER TABLE ${op.table}
|
|
81
|
+
ADD COLUMN ${op.field} ${fieldToSQL(op.fieldMeta)};`;
|
|
82
|
+
|
|
83
|
+
case "RemoveColumn":
|
|
84
|
+
return `ALTER TABLE ${op.model}
|
|
85
|
+
DROP COLUMN ${op.field};`
|
|
86
|
+
|
|
87
|
+
case "DropTable":
|
|
88
|
+
return `DROP TABLE ${op.model};`;
|
|
89
|
+
|
|
90
|
+
case "CreateIndex":
|
|
91
|
+
return createIndexSQL(op.table, op.index);
|
|
92
|
+
|
|
93
|
+
case "DropIndex":
|
|
94
|
+
return dropIndexSQL(op.table, op.index);
|
|
95
|
+
|
|
96
|
+
default:
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
export function requiresRebuild(op: any): boolean {
|
|
103
|
+
return [
|
|
104
|
+
"AddColumn",
|
|
105
|
+
"RemoveColumn",
|
|
106
|
+
"RenameColumn",
|
|
107
|
+
"AddConstraint",
|
|
108
|
+
"RemoveConstraint",
|
|
109
|
+
"AlterField",
|
|
110
|
+
].includes(op.type);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
export function introspectTable(tableName: string): ModelMeta {
|
|
117
|
+
const columns = db
|
|
118
|
+
.prepare(`PRAGMA table_info("${tableName}")`)
|
|
119
|
+
.all();
|
|
120
|
+
|
|
121
|
+
const fks = db
|
|
122
|
+
.prepare(`PRAGMA foreign_key_list("${tableName}")`)
|
|
123
|
+
.all();
|
|
124
|
+
|
|
125
|
+
const fields: any = {};
|
|
126
|
+
|
|
127
|
+
for (const col of columns) {
|
|
128
|
+
fields[col.name] = {
|
|
129
|
+
name: col.name,
|
|
130
|
+
type: col.pk === 1 ? "primary" : "string",
|
|
131
|
+
default: col.default,
|
|
132
|
+
options: col.options,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const fk of fks) {
|
|
137
|
+
fields[fk.from].type = "foreignkey";
|
|
138
|
+
fields[fk.from].options = {
|
|
139
|
+
to: fk.table,
|
|
140
|
+
toField: fk.to,
|
|
141
|
+
onDelete: fk.on_delete,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
tableName,
|
|
147
|
+
fields,
|
|
148
|
+
indexes: {},
|
|
149
|
+
constraints: {},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function applyOpToSchema(
|
|
154
|
+
state: ModelMeta,
|
|
155
|
+
op: any
|
|
156
|
+
): ModelMeta {
|
|
157
|
+
const next = structuredClone(state);
|
|
158
|
+
|
|
159
|
+
switch (op.type) {
|
|
160
|
+
case "AddColumn":
|
|
161
|
+
next.fields[op.field] = op.fieldMeta;
|
|
162
|
+
break;
|
|
163
|
+
|
|
164
|
+
case "RemoveColumn":
|
|
165
|
+
delete next.fields[op.field];
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
case "RenameColumn":
|
|
169
|
+
next.fields[op.to] = {
|
|
170
|
+
...next.fields[op.from],
|
|
171
|
+
name: op.to,
|
|
172
|
+
};
|
|
173
|
+
delete next.fields[op.from];
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case "AddConstraint":
|
|
177
|
+
next.constraints[op.constraint.name] = op.constraint;
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case "RemoveConstraint":
|
|
181
|
+
delete next.constraints[op.name];
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return next;
|
|
186
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { FieldMeta } from "../metadata/types";
|
|
2
|
+
import { getAllModels, getModelFields } from "../metadata/model-registry";
|
|
3
|
+
import { indexName } from "../metadata";
|
|
4
|
+
|
|
5
|
+
export function buildSchemaState() {
|
|
6
|
+
const models: any = {};
|
|
7
|
+
const modelIndexes: any = {};
|
|
8
|
+
for (const model of getAllModels()) {
|
|
9
|
+
const meta = model[Symbol.for("orm:model_meta")];
|
|
10
|
+
const modelFields = getModelFields(meta.tableName);
|
|
11
|
+
const fields = modelFields.fields;
|
|
12
|
+
|
|
13
|
+
const indexes = [...(meta.indexes ?? [])];
|
|
14
|
+
for (const field of fields) {
|
|
15
|
+
// primary key
|
|
16
|
+
if (field.type === "primary") {
|
|
17
|
+
indexes.push({
|
|
18
|
+
name: indexName(meta.tableName, [field.name], true),
|
|
19
|
+
fields: [field.name],
|
|
20
|
+
unique: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// unique field
|
|
25
|
+
if (field.options?.unique) {
|
|
26
|
+
indexes.push({
|
|
27
|
+
name: indexName(meta.tableName, [field.name], true),
|
|
28
|
+
fields: [field.name],
|
|
29
|
+
unique: true,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// foreign key
|
|
34
|
+
// if (field.type === "foreignkey") {
|
|
35
|
+
// indexes.push({
|
|
36
|
+
// name: indexName(meta.tableName, [field.name], false),
|
|
37
|
+
// fields: [field.name],
|
|
38
|
+
// unique: false,
|
|
39
|
+
// });
|
|
40
|
+
// }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
models[meta.tableName] = {
|
|
46
|
+
tableName: meta.tableName,
|
|
47
|
+
fields: Object.fromEntries(
|
|
48
|
+
modelFields.fields.map((f: FieldMeta) => [
|
|
49
|
+
f.name,
|
|
50
|
+
{
|
|
51
|
+
name: f.name,
|
|
52
|
+
type: f.type,
|
|
53
|
+
default: f.default ?? null,
|
|
54
|
+
options: f.options ?? {},
|
|
55
|
+
},
|
|
56
|
+
])
|
|
57
|
+
),
|
|
58
|
+
indexes: indexes,
|
|
59
|
+
constraints: [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
version: 1,
|
|
65
|
+
dialect: "generic",
|
|
66
|
+
models,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function reverseOp(op: any): any {
|
|
71
|
+
switch (op.type) {
|
|
72
|
+
case "AddColumn":
|
|
73
|
+
return {
|
|
74
|
+
type: "RemoveColumn",
|
|
75
|
+
model: op.model,
|
|
76
|
+
field: op.field,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
case "RemoveColumn":
|
|
80
|
+
return {
|
|
81
|
+
type: "AddColumn",
|
|
82
|
+
model: op.model,
|
|
83
|
+
field: op.field,
|
|
84
|
+
fieldMeta: op.fieldMeta,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
case "RenameColumn":
|
|
88
|
+
return {
|
|
89
|
+
type: "RenameColumn",
|
|
90
|
+
model: op.model,
|
|
91
|
+
from: op.to,
|
|
92
|
+
to: op.from,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
case "RenameTable":
|
|
96
|
+
return {
|
|
97
|
+
type: "RenameTable",
|
|
98
|
+
from: op.to,
|
|
99
|
+
to: op.from,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
case "CreateTable":
|
|
103
|
+
return {
|
|
104
|
+
type: "DropTable",
|
|
105
|
+
model: op.meta.tableName, // ✅ FIX
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
case "DropTable":
|
|
109
|
+
return {
|
|
110
|
+
type: "CreateTable",
|
|
111
|
+
meta: op.meta, // model name is inside meta
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
case "CreateIndex":
|
|
115
|
+
return {
|
|
116
|
+
type: "DropIndex",
|
|
117
|
+
index: op.index
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
case "DropIndex":
|
|
121
|
+
return {
|
|
122
|
+
type: "CreateIndex",
|
|
123
|
+
index: op.index
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
case "AddConstraint":
|
|
128
|
+
return {
|
|
129
|
+
type: "RemoveConstraint",
|
|
130
|
+
index: op.index
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
default:
|
|
135
|
+
throw new Error(`No reverse for ${op.type}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const STATE_PATH = path.resolve("src/orm/migrations/_state.json");
|
|
5
|
+
|
|
6
|
+
export function loadState() {
|
|
7
|
+
if (!fs.existsSync(STATE_PATH)) {
|
|
8
|
+
return {
|
|
9
|
+
version: 1,
|
|
10
|
+
dialect: "generic",
|
|
11
|
+
models: {},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return JSON.parse(fs.readFileSync(STATE_PATH, "utf-8"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function saveState(state: any) {
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
STATE_PATH,
|
|
21
|
+
JSON.stringify(state, null, 2)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export function writeMigration(ops: any[]) {
|
|
5
|
+
if (!ops.length) return null;
|
|
6
|
+
|
|
7
|
+
const ts = Date.now();
|
|
8
|
+
const name = `${ts}_auto.ts`;
|
|
9
|
+
const file = path.resolve("src/orm/migrations", name);
|
|
10
|
+
|
|
11
|
+
fs.writeFileSync(
|
|
12
|
+
file,
|
|
13
|
+
`export default {
|
|
14
|
+
up: ${JSON.stringify(ops, null, 2)},
|
|
15
|
+
down: [],
|
|
16
|
+
};
|
|
17
|
+
`
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return name;
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// orm/syncdb.ts
|
|
2
|
+
import { getAllModels, getModelFields } from "./metadata/model-registry";
|
|
3
|
+
import { getModelMeta } from "./metadata/get-meta";
|
|
4
|
+
import { fieldToSQL } from "./db/sql-types";
|
|
5
|
+
import { db } from "./db/sqlite";
|
|
6
|
+
|
|
7
|
+
export async function syncdb() {
|
|
8
|
+
for (const model of getAllModels()) {
|
|
9
|
+
const meta = getModelMeta(model);
|
|
10
|
+
const modelFields = getModelFields(meta.tableName)
|
|
11
|
+
const columns: string[] = [];
|
|
12
|
+
for (const field of modelFields.fields) {
|
|
13
|
+
const sqlType = fieldToSQL(field);
|
|
14
|
+
columns.push(`${field.name} ${sqlType}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const sql = `
|
|
18
|
+
CREATE TABLE IF NOT EXISTS ${meta.tableName} (
|
|
19
|
+
${columns.join(",\n")}
|
|
20
|
+
);
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
db.exec(sql);
|
|
24
|
+
}
|
|
25
|
+
}
|