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,186 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
|
4
|
+
|
|
5
|
+
const TOAST_LIMIT = 1;
|
|
6
|
+
const TOAST_REMOVE_DELAY = 1000000;
|
|
7
|
+
|
|
8
|
+
type ToasterToast = ToastProps & {
|
|
9
|
+
id: string;
|
|
10
|
+
title?: React.ReactNode;
|
|
11
|
+
description?: React.ReactNode;
|
|
12
|
+
action?: ToastActionElement;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const actionTypes = {
|
|
16
|
+
ADD_TOAST: "ADD_TOAST",
|
|
17
|
+
UPDATE_TOAST: "UPDATE_TOAST",
|
|
18
|
+
DISMISS_TOAST: "DISMISS_TOAST",
|
|
19
|
+
REMOVE_TOAST: "REMOVE_TOAST",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
let count = 0;
|
|
23
|
+
|
|
24
|
+
function genId() {
|
|
25
|
+
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
|
26
|
+
return count.toString();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ActionType = typeof actionTypes;
|
|
30
|
+
|
|
31
|
+
type Action =
|
|
32
|
+
| {
|
|
33
|
+
type: ActionType["ADD_TOAST"];
|
|
34
|
+
toast: ToasterToast;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
type: ActionType["UPDATE_TOAST"];
|
|
38
|
+
toast: Partial<ToasterToast>;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
type: ActionType["DISMISS_TOAST"];
|
|
42
|
+
toastId?: ToasterToast["id"];
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
type: ActionType["REMOVE_TOAST"];
|
|
46
|
+
toastId?: ToasterToast["id"];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
interface State {
|
|
50
|
+
toasts: ToasterToast[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
54
|
+
|
|
55
|
+
const addToRemoveQueue = (toastId: string) => {
|
|
56
|
+
if (toastTimeouts.has(toastId)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const timeout = setTimeout(() => {
|
|
61
|
+
toastTimeouts.delete(toastId);
|
|
62
|
+
dispatch({
|
|
63
|
+
type: "REMOVE_TOAST",
|
|
64
|
+
toastId: toastId,
|
|
65
|
+
});
|
|
66
|
+
}, TOAST_REMOVE_DELAY);
|
|
67
|
+
|
|
68
|
+
toastTimeouts.set(toastId, timeout);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const reducer = (state: State, action: Action): State => {
|
|
72
|
+
switch (action.type) {
|
|
73
|
+
case "ADD_TOAST":
|
|
74
|
+
return {
|
|
75
|
+
...state,
|
|
76
|
+
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
case "UPDATE_TOAST":
|
|
80
|
+
return {
|
|
81
|
+
...state,
|
|
82
|
+
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
case "DISMISS_TOAST": {
|
|
86
|
+
const { toastId } = action;
|
|
87
|
+
|
|
88
|
+
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
|
89
|
+
// but I'll keep it here for simplicity
|
|
90
|
+
if (toastId) {
|
|
91
|
+
addToRemoveQueue(toastId);
|
|
92
|
+
} else {
|
|
93
|
+
state.toasts.forEach((toast) => {
|
|
94
|
+
addToRemoveQueue(toast.id);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...state,
|
|
100
|
+
toasts: state.toasts.map((t) =>
|
|
101
|
+
t.id === toastId || toastId === undefined
|
|
102
|
+
? {
|
|
103
|
+
...t,
|
|
104
|
+
open: false,
|
|
105
|
+
}
|
|
106
|
+
: t,
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
case "REMOVE_TOAST":
|
|
111
|
+
if (action.toastId === undefined) {
|
|
112
|
+
return {
|
|
113
|
+
...state,
|
|
114
|
+
toasts: [],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
...state,
|
|
119
|
+
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const listeners: Array<(state: State) => void> = [];
|
|
125
|
+
|
|
126
|
+
let memoryState: State = { toasts: [] };
|
|
127
|
+
|
|
128
|
+
function dispatch(action: Action) {
|
|
129
|
+
memoryState = reducer(memoryState, action);
|
|
130
|
+
listeners.forEach((listener) => {
|
|
131
|
+
listener(memoryState);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
type Toast = Omit<ToasterToast, "id">;
|
|
136
|
+
|
|
137
|
+
function toast({ ...props }: Toast) {
|
|
138
|
+
const id = genId();
|
|
139
|
+
|
|
140
|
+
const update = (props: ToasterToast) =>
|
|
141
|
+
dispatch({
|
|
142
|
+
type: "UPDATE_TOAST",
|
|
143
|
+
toast: { ...props, id },
|
|
144
|
+
});
|
|
145
|
+
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
|
146
|
+
|
|
147
|
+
dispatch({
|
|
148
|
+
type: "ADD_TOAST",
|
|
149
|
+
toast: {
|
|
150
|
+
...props,
|
|
151
|
+
id,
|
|
152
|
+
open: true,
|
|
153
|
+
onOpenChange: (open) => {
|
|
154
|
+
if (!open) dismiss();
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
id: id,
|
|
161
|
+
dismiss,
|
|
162
|
+
update,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function useToast() {
|
|
167
|
+
const [state, setState] = React.useState<State>(memoryState);
|
|
168
|
+
|
|
169
|
+
React.useEffect(() => {
|
|
170
|
+
listeners.push(setState);
|
|
171
|
+
return () => {
|
|
172
|
+
const index = listeners.indexOf(setState);
|
|
173
|
+
if (index > -1) {
|
|
174
|
+
listeners.splice(index, 1);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}, [state]);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
...state,
|
|
181
|
+
toast,
|
|
182
|
+
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export { useToast, toast };
|
package/src/index.css
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap');
|
|
6
|
+
|
|
7
|
+
@layer base {
|
|
8
|
+
:root {
|
|
9
|
+
--background: 222 47% 6%;
|
|
10
|
+
--foreground: 210 40% 98%;
|
|
11
|
+
|
|
12
|
+
--card: 222 47% 8%;
|
|
13
|
+
--card-foreground: 210 40% 98%;
|
|
14
|
+
|
|
15
|
+
--popover: 222 47% 10%;
|
|
16
|
+
--popover-foreground: 210 40% 98%;
|
|
17
|
+
|
|
18
|
+
--primary: 173 80% 40%;
|
|
19
|
+
--primary-foreground: 222 47% 6%;
|
|
20
|
+
|
|
21
|
+
--secondary: 222 47% 12%;
|
|
22
|
+
--secondary-foreground: 210 40% 98%;
|
|
23
|
+
|
|
24
|
+
--muted: 222 47% 14%;
|
|
25
|
+
--muted-foreground: 215 20% 55%;
|
|
26
|
+
|
|
27
|
+
--accent: 173 80% 40%;
|
|
28
|
+
--accent-foreground: 222 47% 6%;
|
|
29
|
+
|
|
30
|
+
--destructive: 0 62% 50%;
|
|
31
|
+
--destructive-foreground: 210 40% 98%;
|
|
32
|
+
|
|
33
|
+
--border: 222 47% 16%;
|
|
34
|
+
--input: 222 47% 14%;
|
|
35
|
+
--ring: 173 80% 40%;
|
|
36
|
+
|
|
37
|
+
--radius: 0.5rem;
|
|
38
|
+
|
|
39
|
+
--sidebar-background: 222 47% 5%;
|
|
40
|
+
--sidebar-foreground: 210 40% 80%;
|
|
41
|
+
--sidebar-primary: 173 80% 40%;
|
|
42
|
+
--sidebar-primary-foreground: 222 47% 6%;
|
|
43
|
+
--sidebar-accent: 222 47% 10%;
|
|
44
|
+
--sidebar-accent-foreground: 210 40% 98%;
|
|
45
|
+
--sidebar-border: 222 47% 12%;
|
|
46
|
+
--sidebar-ring: 173 80% 40%;
|
|
47
|
+
|
|
48
|
+
/* Custom tokens */
|
|
49
|
+
--success: 142 76% 36%;
|
|
50
|
+
--success-foreground: 210 40% 98%;
|
|
51
|
+
--warning: 38 92% 50%;
|
|
52
|
+
--warning-foreground: 222 47% 6%;
|
|
53
|
+
--info: 199 89% 48%;
|
|
54
|
+
--info-foreground: 222 47% 6%;
|
|
55
|
+
|
|
56
|
+
--font-mono: 'JetBrains Mono', monospace;
|
|
57
|
+
--font-sans: 'Inter', system-ui, sans-serif;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.dark {
|
|
61
|
+
--background: 222 47% 6%;
|
|
62
|
+
--foreground: 210 40% 98%;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@layer base {
|
|
67
|
+
* {
|
|
68
|
+
@apply border-border;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
body {
|
|
72
|
+
@apply bg-background text-foreground font-sans antialiased;
|
|
73
|
+
font-family: var(--font-sans);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
code, pre, .font-mono {
|
|
77
|
+
font-family: var(--font-mono);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@layer components {
|
|
82
|
+
.admin-card {
|
|
83
|
+
@apply bg-card border border-border rounded-lg p-4 transition-all duration-200;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.admin-card:hover {
|
|
87
|
+
@apply border-primary/30;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.stat-card {
|
|
91
|
+
@apply admin-card relative overflow-hidden;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.stat-card::before {
|
|
95
|
+
content: '';
|
|
96
|
+
@apply absolute top-0 left-0 w-1 h-full bg-primary;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.model-badge {
|
|
100
|
+
@apply inline-flex items-center px-2 py-0.5 rounded text-xs font-medium font-mono;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.field-type-string {
|
|
104
|
+
@apply bg-info/20 text-info;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.field-type-number {
|
|
108
|
+
@apply bg-warning/20 text-warning;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.field-type-boolean {
|
|
112
|
+
@apply bg-success/20 text-success;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.field-type-date {
|
|
116
|
+
@apply bg-primary/20 text-primary;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.glow-effect {
|
|
120
|
+
box-shadow: 0 0 20px hsl(var(--primary) / 0.15);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@layer utilities {
|
|
125
|
+
.scrollbar-thin {
|
|
126
|
+
scrollbar-width: thin;
|
|
127
|
+
scrollbar-color: hsl(var(--muted)) transparent;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.scrollbar-thin::-webkit-scrollbar {
|
|
131
|
+
width: 6px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.scrollbar-thin::-webkit-scrollbar-track {
|
|
135
|
+
background: transparent;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.scrollbar-thin::-webkit-scrollbar-thumb {
|
|
139
|
+
background-color: hsl(var(--muted));
|
|
140
|
+
border-radius: 3px;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// ORM-like model definitions
|
|
2
|
+
|
|
3
|
+
export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'text' | 'email' | 'select';
|
|
4
|
+
|
|
5
|
+
export interface FieldDefinition {
|
|
6
|
+
name: string;
|
|
7
|
+
type: FieldType;
|
|
8
|
+
label: string;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
default?: unknown;
|
|
11
|
+
options?: string[]; // for select type
|
|
12
|
+
maxLength?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ModelDefinition {
|
|
16
|
+
name: string;
|
|
17
|
+
displayName: string;
|
|
18
|
+
icon: string;
|
|
19
|
+
fields: FieldDefinition[];
|
|
20
|
+
primaryKey: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Define your models here (like Django models)
|
|
24
|
+
export const models: ModelDefinition[] = [
|
|
25
|
+
{
|
|
26
|
+
name: 'users',
|
|
27
|
+
displayName: 'Users',
|
|
28
|
+
icon: 'Users',
|
|
29
|
+
primaryKey: 'id',
|
|
30
|
+
fields: [
|
|
31
|
+
{ name: 'id', type: 'number', label: 'ID', required: true },
|
|
32
|
+
{ name: 'username', type: 'string', label: 'Username', required: true, maxLength: 50 },
|
|
33
|
+
{ name: 'email', type: 'email', label: 'Email', required: true },
|
|
34
|
+
{ name: 'role', type: 'select', label: 'Role', options: ['admin', 'user', 'editor'], default: 'user' },
|
|
35
|
+
{ name: 'is_active', type: 'boolean', label: 'Active', default: true },
|
|
36
|
+
{ name: 'created_at', type: 'date', label: 'Created At' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'posts',
|
|
41
|
+
displayName: 'Posts',
|
|
42
|
+
icon: 'FileText',
|
|
43
|
+
primaryKey: 'id',
|
|
44
|
+
fields: [
|
|
45
|
+
{ name: 'id', type: 'number', label: 'ID', required: true },
|
|
46
|
+
{ name: 'title', type: 'string', label: 'Title', required: true, maxLength: 200 },
|
|
47
|
+
{ name: 'content', type: 'text', label: 'Content' },
|
|
48
|
+
{ name: 'author_id', type: 'number', label: 'Author ID', required: true },
|
|
49
|
+
{ name: 'published', type: 'boolean', label: 'Published', default: false },
|
|
50
|
+
{ name: 'created_at', type: 'date', label: 'Created At' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'products',
|
|
55
|
+
displayName: 'Products',
|
|
56
|
+
icon: 'Package',
|
|
57
|
+
primaryKey: 'id',
|
|
58
|
+
fields: [
|
|
59
|
+
{ name: 'id', type: 'number', label: 'ID', required: true },
|
|
60
|
+
{ name: 'name', type: 'string', label: 'Name', required: true },
|
|
61
|
+
{ name: 'price', type: 'number', label: 'Price', required: true },
|
|
62
|
+
{ name: 'category', type: 'select', label: 'Category', options: ['electronics', 'clothing', 'food', 'other'] },
|
|
63
|
+
{ name: 'in_stock', type: 'boolean', label: 'In Stock', default: true },
|
|
64
|
+
{ name: 'created_at', type: 'date', label: 'Created At' },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'orders',
|
|
69
|
+
displayName: 'Orders',
|
|
70
|
+
icon: 'ShoppingCart',
|
|
71
|
+
primaryKey: 'id',
|
|
72
|
+
fields: [
|
|
73
|
+
{ name: 'id', type: 'number', label: 'ID', required: true },
|
|
74
|
+
{ name: 'user_id', type: 'number', label: 'User ID', required: true },
|
|
75
|
+
{ name: 'total', type: 'number', label: 'Total', required: true },
|
|
76
|
+
{ name: 'status', type: 'select', label: 'Status', options: ['pending', 'processing', 'shipped', 'delivered', 'cancelled'] },
|
|
77
|
+
{ name: 'created_at', type: 'date', label: 'Created At' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'table',
|
|
82
|
+
displayName: 'Table',
|
|
83
|
+
primaryKey: 'id',
|
|
84
|
+
fields: [
|
|
85
|
+
{ name: 'id', type: 'number', label: 'ID', required: true },
|
|
86
|
+
{ name: 'name', type: 'string', label: 'Name', required: true },
|
|
87
|
+
],
|
|
88
|
+
icon: ""
|
|
89
|
+
}
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
export function getModel(name: string): ModelDefinition | undefined {
|
|
93
|
+
return models.find(m => m.name === name);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Generate mock data for a model
|
|
97
|
+
export function generateMockData(model: ModelDefinition, count: number = 10): Record<string, unknown>[] {
|
|
98
|
+
const data: Record<string, unknown>[] = [];
|
|
99
|
+
|
|
100
|
+
for (let i = 1; i <= count; i++) {
|
|
101
|
+
const record: Record<string, unknown> = {};
|
|
102
|
+
|
|
103
|
+
for (const field of model.fields) {
|
|
104
|
+
switch (field.type) {
|
|
105
|
+
case 'number':
|
|
106
|
+
record[field.name] = field.name === 'id' ? i :
|
|
107
|
+
field.name.includes('price') || field.name.includes('total') ?
|
|
108
|
+
Math.floor(Math.random() * 1000) + 10 :
|
|
109
|
+
Math.floor(Math.random() * 100) + 1;
|
|
110
|
+
break;
|
|
111
|
+
case 'string':
|
|
112
|
+
case 'text':
|
|
113
|
+
record[field.name] = `${field.label} ${i}`;
|
|
114
|
+
break;
|
|
115
|
+
case 'email':
|
|
116
|
+
record[field.name] = `user${i}@example.com`;
|
|
117
|
+
break;
|
|
118
|
+
case 'boolean':
|
|
119
|
+
record[field.name] = Math.random() > 0.3;
|
|
120
|
+
break;
|
|
121
|
+
case 'date':
|
|
122
|
+
const date = new Date();
|
|
123
|
+
date.setDate(date.getDate() - Math.floor(Math.random() * 30));
|
|
124
|
+
record[field.name] = date.toISOString().split('T')[0];
|
|
125
|
+
break;
|
|
126
|
+
case 'select':
|
|
127
|
+
if (field.options && field.options.length > 0) {
|
|
128
|
+
record[field.name] = field.options[Math.floor(Math.random() * field.options.length)];
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
data.push(record);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return data;
|
|
138
|
+
}
|
package/src/lib/utils.ts
ADDED
package/src/main.tsx
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { diffStates } from "../migration-engine/diff";
|
|
4
|
+
import { buildSchemaState } from "../schema/build";
|
|
5
|
+
import { loadState } from "../schema/state";
|
|
6
|
+
import { reverseOp } from "../schema/build";
|
|
7
|
+
|
|
8
|
+
export function makemigrations() {
|
|
9
|
+
|
|
10
|
+
const migrationsDir = path.resolve(
|
|
11
|
+
process.cwd(),
|
|
12
|
+
"migrations"
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
// 🔑 ENSURE DIRECTORY EXISTS
|
|
16
|
+
fs.mkdirSync(migrationsDir, { recursive: true });
|
|
17
|
+
|
|
18
|
+
const prevState = loadState();
|
|
19
|
+
|
|
20
|
+
const currentState = buildSchemaState();
|
|
21
|
+
|
|
22
|
+
const ops = diffStates(prevState, currentState);
|
|
23
|
+
|
|
24
|
+
const destructiveOps = ops.filter(op =>
|
|
25
|
+
["DropTable", "RemoveColumn"].includes(op.type)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (destructiveOps.length) {
|
|
29
|
+
console.warn(
|
|
30
|
+
"\n⚠️ Destructive changes detected:\n",
|
|
31
|
+
destructiveOps
|
|
32
|
+
);
|
|
33
|
+
console.warn(
|
|
34
|
+
"These operations will NOT be applied automatically."
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (ops.length === 0) {
|
|
39
|
+
console.log("No changes detected");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const filename = `${process.cwd()}/migrations/${Date.now()}_auto.ts`;
|
|
44
|
+
|
|
45
|
+
const up = ops;
|
|
46
|
+
const down = [...up].reverse().map(reverseOp);
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
fs.writeFileSync(
|
|
50
|
+
filename,
|
|
51
|
+
`export default {
|
|
52
|
+
up: ${JSON.stringify(up, null, 2)},
|
|
53
|
+
down: ${JSON.stringify(down, null, 2)}
|
|
54
|
+
};`
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
`${process.cwd()}/migrations/_state.json`,
|
|
59
|
+
JSON.stringify(currentState, null, 2)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
console.log(`Created migration ${filename}`);
|
|
63
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { db } from "../db/sqlite";
|
|
4
|
+
import { opToSQL } from "../migration-engine/operations";
|
|
5
|
+
import { requiresRebuild, introspectTable, rebuildTable, applyOpToSchema } from "../migration-engine/operations";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async function applyMigration(file: string) {
|
|
9
|
+
const mod = await importMigration(file);
|
|
10
|
+
for (const op of mod.up) {
|
|
11
|
+
if (requiresRebuild(op)) {
|
|
12
|
+
const oldState = introspectTable(op.model);
|
|
13
|
+
const newState = applyOpToSchema(oldState, op);
|
|
14
|
+
const sqlStatements = rebuildTable(
|
|
15
|
+
op.model,
|
|
16
|
+
oldState,
|
|
17
|
+
newState
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
db.transaction(() => {
|
|
21
|
+
for (const sql of sqlStatements) {
|
|
22
|
+
console.log(sql);
|
|
23
|
+
db.exec(sql);
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
}
|
|
27
|
+
else
|
|
28
|
+
{
|
|
29
|
+
const sql = opToSQL(op);
|
|
30
|
+
if (sql) db.exec(sql);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
db.prepare(
|
|
35
|
+
`INSERT INTO orm_migrations VALUES (?, ?)`
|
|
36
|
+
).run(file, Date.now());
|
|
37
|
+
|
|
38
|
+
console.log(`Applied ${file}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function rollbackMigration(file: string) {
|
|
42
|
+
const mod = await importMigration(file);
|
|
43
|
+
|
|
44
|
+
for (const op of mod.down) {
|
|
45
|
+
if (requiresRebuild(op)) {
|
|
46
|
+
const oldState = introspectTable(op.table);
|
|
47
|
+
const newState = applyOpToSchema(oldState, op);
|
|
48
|
+
|
|
49
|
+
const sqlStatements = rebuildTable(
|
|
50
|
+
op.table,
|
|
51
|
+
oldState,
|
|
52
|
+
newState
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
db.transaction(() => {
|
|
56
|
+
for (const sql of sqlStatements) {
|
|
57
|
+
db.exec(sql);
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
{
|
|
63
|
+
const sql = opToSQL(op);
|
|
64
|
+
if (sql) db.exec(sql);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
db.prepare(
|
|
69
|
+
`DELETE FROM orm_migrations WHERE name = ?`
|
|
70
|
+
).run(file);
|
|
71
|
+
|
|
72
|
+
console.log(`Rolled back ${file}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function importMigration(file: string) {
|
|
76
|
+
const modulePath = new URL(
|
|
77
|
+
`${process.cwd()}/migrations/${file}`,
|
|
78
|
+
import.meta.url
|
|
79
|
+
).href;
|
|
80
|
+
|
|
81
|
+
return (await import(modulePath)).default;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
export async function migrate({ to }: { to?: string } = {}) {
|
|
86
|
+
db.exec(`
|
|
87
|
+
CREATE TABLE IF NOT EXISTS orm_migrations (
|
|
88
|
+
name TEXT PRIMARY KEY,
|
|
89
|
+
applied_at INTEGER
|
|
90
|
+
);
|
|
91
|
+
`);
|
|
92
|
+
|
|
93
|
+
const applied = db
|
|
94
|
+
.prepare(`SELECT name FROM orm_migrations ORDER BY applied_at`)
|
|
95
|
+
.all()
|
|
96
|
+
.map((r: any) => r.name);
|
|
97
|
+
|
|
98
|
+
const migrationsDir = path.resolve(`${process.cwd()}/migrations`);
|
|
99
|
+
|
|
100
|
+
const allFiles = fs
|
|
101
|
+
.readdirSync(migrationsDir)
|
|
102
|
+
.filter(f => f.endsWith(".ts") && f !== "_state.json")
|
|
103
|
+
.sort();
|
|
104
|
+
|
|
105
|
+
// DEFAULT: migrate all up
|
|
106
|
+
if (!to) {
|
|
107
|
+
for (const file of allFiles) {
|
|
108
|
+
if (applied.includes(file)) continue;
|
|
109
|
+
await applyMigration(file);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ROLLBACK
|
|
115
|
+
if (to === "zero") {
|
|
116
|
+
for (const file of applied.reverse()) {
|
|
117
|
+
await rollbackMigration(file);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ROLLBACK TO TARGET
|
|
123
|
+
for (const file of applied.reverse()) {
|
|
124
|
+
if (file === to) break;
|
|
125
|
+
await rollbackMigration(file);
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/orm/cli.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/orm/cli.ts
|
|
2
|
+
import "../models"
|
|
3
|
+
import { makemigrations } from "./cli/makemigrations";
|
|
4
|
+
import { migrate } from "./cli/migrate";
|
|
5
|
+
|
|
6
|
+
// IMPORTANT: import models so decorators run
|
|
7
|
+
import "../models";
|
|
8
|
+
|
|
9
|
+
const command = process.argv[2];
|
|
10
|
+
const to = process.argv[3];
|
|
11
|
+
|
|
12
|
+
switch (command) {
|
|
13
|
+
case "makemigrations":
|
|
14
|
+
makemigrations();
|
|
15
|
+
break;
|
|
16
|
+
|
|
17
|
+
case "migrate":
|
|
18
|
+
migrate({ to });
|
|
19
|
+
break;
|
|
20
|
+
|
|
21
|
+
default:
|
|
22
|
+
console.error(`
|
|
23
|
+
Unknown command: ${command}
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
npm run orm makemigrations
|
|
27
|
+
npm run orm migrate
|
|
28
|
+
`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|