sveltekit-admin 0.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.
- package/README.md +172 -0
- package/dist/admin.d.ts +227 -0
- package/dist/admin.js +369 -0
- package/dist/components/AdminForm.svelte +423 -0
- package/dist/components/AdminForm.svelte.d.ts +30 -0
- package/dist/components/AdminLayout.svelte +328 -0
- package/dist/components/AdminLayout.svelte.d.ts +20 -0
- package/dist/components/DataTable.svelte +573 -0
- package/dist/components/DataTable.svelte.d.ts +25 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +16 -0
- package/dist/server/auth/guard.d.ts +36 -0
- package/dist/server/auth/guard.js +38 -0
- package/dist/server/auth/index.d.ts +1 -0
- package/dist/server/auth/index.js +1 -0
- package/dist/server/crud/index.d.ts +1 -0
- package/dist/server/crud/index.js +1 -0
- package/dist/server/crud/operations.d.ts +87 -0
- package/dist/server/crud/operations.js +276 -0
- package/dist/server/introspection/index.d.ts +1 -0
- package/dist/server/introspection/index.js +1 -0
- package/dist/server/introspection/parser.d.ts +51 -0
- package/dist/server/introspection/parser.js +193 -0
- package/package.json +83 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* better-auth integration for admin panel
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Default admin check - looks for role field
|
|
6
|
+
*/
|
|
7
|
+
export function defaultAdminCheck(user, adminRole = 'admin') {
|
|
8
|
+
if (!user || typeof user !== 'object')
|
|
9
|
+
return false;
|
|
10
|
+
const u = user;
|
|
11
|
+
// Check common role field names
|
|
12
|
+
if (u.role === adminRole)
|
|
13
|
+
return true;
|
|
14
|
+
if (u.isAdmin === true)
|
|
15
|
+
return true;
|
|
16
|
+
if (Array.isArray(u.roles) && u.roles.includes(adminRole))
|
|
17
|
+
return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create auth guard for server routes
|
|
22
|
+
*/
|
|
23
|
+
export function createAuthGuard(config) {
|
|
24
|
+
const { adminRole = 'admin', adminCheck } = config;
|
|
25
|
+
return async (locals) => {
|
|
26
|
+
// Get session from better-auth
|
|
27
|
+
const session = locals.session;
|
|
28
|
+
const user = locals.user;
|
|
29
|
+
if (!session || !user) {
|
|
30
|
+
return { authorized: false, user: null };
|
|
31
|
+
}
|
|
32
|
+
// Check if user is admin
|
|
33
|
+
const isAdmin = adminCheck
|
|
34
|
+
? await adminCheck(user)
|
|
35
|
+
: defaultAdminCheck(user, adminRole);
|
|
36
|
+
return { authorized: isAdmin, user };
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './guard.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './guard.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './operations.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './operations.js';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRUD Operations Generator
|
|
3
|
+
* Creates type-safe Prisma operations for admin panel
|
|
4
|
+
*/
|
|
5
|
+
import type { PrismaModel } from '../introspection/parser.js';
|
|
6
|
+
export interface ListOptions {
|
|
7
|
+
page?: number;
|
|
8
|
+
perPage?: number;
|
|
9
|
+
search?: string;
|
|
10
|
+
searchFields?: string[];
|
|
11
|
+
orderBy?: string;
|
|
12
|
+
orderDir?: 'asc' | 'desc';
|
|
13
|
+
filters?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface ListResult<T> {
|
|
16
|
+
items: T[];
|
|
17
|
+
total: number;
|
|
18
|
+
page: number;
|
|
19
|
+
perPage: number;
|
|
20
|
+
totalPages: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a Prisma where clause for searching
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildSearchWhere(search: string | undefined, searchFields: string[], model: PrismaModel): Record<string, unknown> | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Build filter conditions from query params
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildFilterWhere(filters: Record<string, unknown> | undefined, model: PrismaModel): Record<string, unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* Create list operation config
|
|
32
|
+
*/
|
|
33
|
+
export declare function createListOperation(model: PrismaModel): {
|
|
34
|
+
modelName: string;
|
|
35
|
+
searchableFields: string[];
|
|
36
|
+
defaultOrderBy: string;
|
|
37
|
+
execute<T>(prisma: {
|
|
38
|
+
[key: string]: {
|
|
39
|
+
findMany: Function;
|
|
40
|
+
count: Function;
|
|
41
|
+
};
|
|
42
|
+
}, options?: ListOptions): Promise<ListResult<T>>;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Create get single record operation
|
|
46
|
+
*/
|
|
47
|
+
export declare function createGetOperation(model: PrismaModel): {
|
|
48
|
+
modelName: string;
|
|
49
|
+
execute<T>(prisma: {
|
|
50
|
+
[key: string]: {
|
|
51
|
+
findUnique: Function;
|
|
52
|
+
};
|
|
53
|
+
}, id: string | number): Promise<T | null>;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Create create operation
|
|
57
|
+
*/
|
|
58
|
+
export declare function createCreateOperation(model: PrismaModel): {
|
|
59
|
+
modelName: string;
|
|
60
|
+
execute<T>(prisma: {
|
|
61
|
+
[key: string]: {
|
|
62
|
+
create: Function;
|
|
63
|
+
};
|
|
64
|
+
}, data: Record<string, unknown>): Promise<T>;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Create update operation
|
|
68
|
+
*/
|
|
69
|
+
export declare function createUpdateOperation(model: PrismaModel): {
|
|
70
|
+
modelName: string;
|
|
71
|
+
execute<T>(prisma: {
|
|
72
|
+
[key: string]: {
|
|
73
|
+
update: Function;
|
|
74
|
+
};
|
|
75
|
+
}, id: string | number, data: Record<string, unknown>): Promise<T>;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Create delete operation
|
|
79
|
+
*/
|
|
80
|
+
export declare function createDeleteOperation(model: PrismaModel): {
|
|
81
|
+
modelName: string;
|
|
82
|
+
execute(prisma: {
|
|
83
|
+
[key: string]: {
|
|
84
|
+
delete: Function;
|
|
85
|
+
};
|
|
86
|
+
}, id: string | number): Promise<void>;
|
|
87
|
+
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRUD Operations Generator
|
|
3
|
+
* Creates type-safe Prisma operations for admin panel
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Build a Prisma where clause for searching
|
|
7
|
+
*/
|
|
8
|
+
export function buildSearchWhere(search, searchFields, model) {
|
|
9
|
+
if (!search || searchFields.length === 0)
|
|
10
|
+
return undefined;
|
|
11
|
+
const stringFields = searchFields.filter(fieldName => {
|
|
12
|
+
const field = model.fields.find(f => f.name === fieldName);
|
|
13
|
+
return field?.type === 'String';
|
|
14
|
+
});
|
|
15
|
+
if (stringFields.length === 0)
|
|
16
|
+
return undefined;
|
|
17
|
+
return {
|
|
18
|
+
OR: stringFields.map(field => ({
|
|
19
|
+
[field]: {
|
|
20
|
+
contains: search,
|
|
21
|
+
mode: 'insensitive'
|
|
22
|
+
}
|
|
23
|
+
}))
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build filter conditions from query params
|
|
28
|
+
*/
|
|
29
|
+
export function buildFilterWhere(filters, model) {
|
|
30
|
+
if (!filters)
|
|
31
|
+
return {};
|
|
32
|
+
const where = {};
|
|
33
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
34
|
+
if (value === undefined || value === null || value === '')
|
|
35
|
+
continue;
|
|
36
|
+
const field = model.fields.find(f => f.name === key);
|
|
37
|
+
if (!field)
|
|
38
|
+
continue;
|
|
39
|
+
// Handle different field types
|
|
40
|
+
switch (field.type) {
|
|
41
|
+
case 'String':
|
|
42
|
+
where[key] = { contains: String(value), mode: 'insensitive' };
|
|
43
|
+
break;
|
|
44
|
+
case 'Int':
|
|
45
|
+
case 'Float':
|
|
46
|
+
case 'Decimal':
|
|
47
|
+
case 'BigInt':
|
|
48
|
+
where[key] = Number(value);
|
|
49
|
+
break;
|
|
50
|
+
case 'Boolean':
|
|
51
|
+
where[key] = value === 'true' || value === true;
|
|
52
|
+
break;
|
|
53
|
+
case 'DateTime':
|
|
54
|
+
// Support date range filters
|
|
55
|
+
if (typeof value === 'object' && value !== null) {
|
|
56
|
+
const dateFilter = {};
|
|
57
|
+
if ('from' in value)
|
|
58
|
+
dateFilter.gte = new Date(value.from);
|
|
59
|
+
if ('to' in value)
|
|
60
|
+
dateFilter.lte = new Date(value.to);
|
|
61
|
+
where[key] = dateFilter;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
where[key] = new Date(String(value));
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
// For enums and relations
|
|
69
|
+
where[key] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return where;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create list operation config
|
|
76
|
+
*/
|
|
77
|
+
export function createListOperation(model) {
|
|
78
|
+
const searchableFields = model.fields
|
|
79
|
+
.filter(f => f.type === 'String' && !f.relation)
|
|
80
|
+
.map(f => f.name);
|
|
81
|
+
return {
|
|
82
|
+
modelName: model.name,
|
|
83
|
+
searchableFields,
|
|
84
|
+
defaultOrderBy: model.primaryKey || 'id',
|
|
85
|
+
async execute(prisma, options = {}) {
|
|
86
|
+
const { page = 1, perPage = 20, search, searchFields = searchableFields, orderBy = model.primaryKey || 'id', orderDir = 'desc', filters } = options;
|
|
87
|
+
const modelKey = model.name.charAt(0).toLowerCase() + model.name.slice(1);
|
|
88
|
+
const prismaModel = prisma[modelKey];
|
|
89
|
+
if (!prismaModel) {
|
|
90
|
+
throw new Error(`Model ${model.name} not found in Prisma client`);
|
|
91
|
+
}
|
|
92
|
+
// Build where clause
|
|
93
|
+
const searchWhere = buildSearchWhere(search, searchFields, model);
|
|
94
|
+
const filterWhere = buildFilterWhere(filters, model);
|
|
95
|
+
const where = {
|
|
96
|
+
...filterWhere,
|
|
97
|
+
...(searchWhere ? searchWhere : {})
|
|
98
|
+
};
|
|
99
|
+
// Build include for relations (limit depth)
|
|
100
|
+
const include = buildInclude(model);
|
|
101
|
+
// Execute queries
|
|
102
|
+
const [items, total] = await Promise.all([
|
|
103
|
+
prismaModel.findMany({
|
|
104
|
+
where,
|
|
105
|
+
include,
|
|
106
|
+
orderBy: { [orderBy]: orderDir },
|
|
107
|
+
skip: (page - 1) * perPage,
|
|
108
|
+
take: perPage
|
|
109
|
+
}),
|
|
110
|
+
prismaModel.count({ where })
|
|
111
|
+
]);
|
|
112
|
+
return {
|
|
113
|
+
items: items,
|
|
114
|
+
total,
|
|
115
|
+
page,
|
|
116
|
+
perPage,
|
|
117
|
+
totalPages: Math.ceil(total / perPage)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Create get single record operation
|
|
124
|
+
*/
|
|
125
|
+
export function createGetOperation(model) {
|
|
126
|
+
return {
|
|
127
|
+
modelName: model.name,
|
|
128
|
+
async execute(prisma, id) {
|
|
129
|
+
const modelKey = model.name.charAt(0).toLowerCase() + model.name.slice(1);
|
|
130
|
+
const prismaModel = prisma[modelKey];
|
|
131
|
+
if (!prismaModel) {
|
|
132
|
+
throw new Error(`Model ${model.name} not found in Prisma client`);
|
|
133
|
+
}
|
|
134
|
+
const primaryKey = model.primaryKey || 'id';
|
|
135
|
+
const include = buildInclude(model);
|
|
136
|
+
// Convert ID to correct type
|
|
137
|
+
const pkField = model.fields.find(f => f.name === primaryKey);
|
|
138
|
+
const typedId = pkField?.type === 'Int' ? parseInt(String(id)) : id;
|
|
139
|
+
return prismaModel.findUnique({
|
|
140
|
+
where: { [primaryKey]: typedId },
|
|
141
|
+
include
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Create create operation
|
|
148
|
+
*/
|
|
149
|
+
export function createCreateOperation(model) {
|
|
150
|
+
return {
|
|
151
|
+
modelName: model.name,
|
|
152
|
+
async execute(prisma, data) {
|
|
153
|
+
const modelKey = model.name.charAt(0).toLowerCase() + model.name.slice(1);
|
|
154
|
+
const prismaModel = prisma[modelKey];
|
|
155
|
+
if (!prismaModel) {
|
|
156
|
+
throw new Error(`Model ${model.name} not found in Prisma client`);
|
|
157
|
+
}
|
|
158
|
+
// Process data - handle relations
|
|
159
|
+
const processedData = processInputData(data, model);
|
|
160
|
+
return prismaModel.create({
|
|
161
|
+
data: processedData
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Create update operation
|
|
168
|
+
*/
|
|
169
|
+
export function createUpdateOperation(model) {
|
|
170
|
+
return {
|
|
171
|
+
modelName: model.name,
|
|
172
|
+
async execute(prisma, id, data) {
|
|
173
|
+
const modelKey = model.name.charAt(0).toLowerCase() + model.name.slice(1);
|
|
174
|
+
const prismaModel = prisma[modelKey];
|
|
175
|
+
if (!prismaModel) {
|
|
176
|
+
throw new Error(`Model ${model.name} not found in Prisma client`);
|
|
177
|
+
}
|
|
178
|
+
const primaryKey = model.primaryKey || 'id';
|
|
179
|
+
const pkField = model.fields.find(f => f.name === primaryKey);
|
|
180
|
+
const typedId = pkField?.type === 'Int' ? parseInt(String(id)) : id;
|
|
181
|
+
// Process data - handle relations
|
|
182
|
+
const processedData = processInputData(data, model);
|
|
183
|
+
return prismaModel.update({
|
|
184
|
+
where: { [primaryKey]: typedId },
|
|
185
|
+
data: processedData
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Create delete operation
|
|
192
|
+
*/
|
|
193
|
+
export function createDeleteOperation(model) {
|
|
194
|
+
return {
|
|
195
|
+
modelName: model.name,
|
|
196
|
+
async execute(prisma, id) {
|
|
197
|
+
const modelKey = model.name.charAt(0).toLowerCase() + model.name.slice(1);
|
|
198
|
+
const prismaModel = prisma[modelKey];
|
|
199
|
+
if (!prismaModel) {
|
|
200
|
+
throw new Error(`Model ${model.name} not found in Prisma client`);
|
|
201
|
+
}
|
|
202
|
+
const primaryKey = model.primaryKey || 'id';
|
|
203
|
+
const pkField = model.fields.find(f => f.name === primaryKey);
|
|
204
|
+
const typedId = pkField?.type === 'Int' ? parseInt(String(id)) : id;
|
|
205
|
+
await prismaModel.delete({
|
|
206
|
+
where: { [primaryKey]: typedId }
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Build include object for relations
|
|
213
|
+
*/
|
|
214
|
+
function buildInclude(model) {
|
|
215
|
+
const include = {};
|
|
216
|
+
for (const field of model.fields) {
|
|
217
|
+
if (field.relation && !field.isList) {
|
|
218
|
+
// Only include single relations, not lists (for performance)
|
|
219
|
+
include[field.name] = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return Object.keys(include).length > 0 ? include : undefined;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Process input data for create/update
|
|
226
|
+
*/
|
|
227
|
+
function processInputData(data, model) {
|
|
228
|
+
const processed = {};
|
|
229
|
+
for (const field of model.fields) {
|
|
230
|
+
// Skip auto-generated fields
|
|
231
|
+
if (field.isId || field.isCreatedAt || field.isUpdatedAt)
|
|
232
|
+
continue;
|
|
233
|
+
const value = data[field.name];
|
|
234
|
+
// Skip undefined values
|
|
235
|
+
if (value === undefined)
|
|
236
|
+
continue;
|
|
237
|
+
// Handle relations
|
|
238
|
+
if (field.relation?.fields) {
|
|
239
|
+
// This is the "owning" side of a relation with foreign key
|
|
240
|
+
// User provides the related ID directly
|
|
241
|
+
const fkField = field.relation.fields[0];
|
|
242
|
+
if (data[fkField] !== undefined) {
|
|
243
|
+
processed[fkField] = data[fkField];
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
// Handle type conversions
|
|
248
|
+
if (value !== null && value !== '') {
|
|
249
|
+
switch (field.type) {
|
|
250
|
+
case 'Int':
|
|
251
|
+
case 'BigInt':
|
|
252
|
+
processed[field.name] = parseInt(String(value));
|
|
253
|
+
break;
|
|
254
|
+
case 'Float':
|
|
255
|
+
case 'Decimal':
|
|
256
|
+
processed[field.name] = parseFloat(String(value));
|
|
257
|
+
break;
|
|
258
|
+
case 'Boolean':
|
|
259
|
+
processed[field.name] = value === true || value === 'true' || value === 'on';
|
|
260
|
+
break;
|
|
261
|
+
case 'DateTime':
|
|
262
|
+
processed[field.name] = new Date(String(value));
|
|
263
|
+
break;
|
|
264
|
+
case 'Json':
|
|
265
|
+
processed[field.name] = typeof value === 'string' ? JSON.parse(value) : value;
|
|
266
|
+
break;
|
|
267
|
+
default:
|
|
268
|
+
processed[field.name] = value;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else if (!field.isRequired) {
|
|
272
|
+
processed[field.name] = null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return processed;
|
|
276
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './parser.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './parser.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma Schema Parser
|
|
3
|
+
* Extracts model information from Prisma schema files
|
|
4
|
+
*/
|
|
5
|
+
export interface PrismaField {
|
|
6
|
+
name: string;
|
|
7
|
+
type: string;
|
|
8
|
+
isRequired: boolean;
|
|
9
|
+
isList: boolean;
|
|
10
|
+
isUnique: boolean;
|
|
11
|
+
isId: boolean;
|
|
12
|
+
isUpdatedAt: boolean;
|
|
13
|
+
isCreatedAt: boolean;
|
|
14
|
+
hasDefault: boolean;
|
|
15
|
+
defaultValue?: string;
|
|
16
|
+
relation?: {
|
|
17
|
+
name?: string;
|
|
18
|
+
model: string;
|
|
19
|
+
fields?: string[];
|
|
20
|
+
references?: string[];
|
|
21
|
+
};
|
|
22
|
+
documentation?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface PrismaModel {
|
|
25
|
+
name: string;
|
|
26
|
+
fields: PrismaField[];
|
|
27
|
+
documentation?: string;
|
|
28
|
+
primaryKey?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface PrismaSchema {
|
|
31
|
+
models: PrismaModel[];
|
|
32
|
+
enums: Map<string, string[]>;
|
|
33
|
+
}
|
|
34
|
+
export declare function parsePrismaSchema(schemaPath: string): PrismaSchema;
|
|
35
|
+
export declare function parseSchemaContent(content: string): PrismaSchema;
|
|
36
|
+
/**
|
|
37
|
+
* Get display fields for a model (fields suitable for list view)
|
|
38
|
+
*/
|
|
39
|
+
export declare function getDisplayFields(model: PrismaModel): PrismaField[];
|
|
40
|
+
/**
|
|
41
|
+
* Get editable fields for a model (fields that can be edited in forms)
|
|
42
|
+
*/
|
|
43
|
+
export declare function getEditableFields(model: PrismaModel): PrismaField[];
|
|
44
|
+
/**
|
|
45
|
+
* Get a human-readable label from a field name
|
|
46
|
+
*/
|
|
47
|
+
export declare function fieldToLabel(fieldName: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Determine the input type for a Prisma field
|
|
50
|
+
*/
|
|
51
|
+
export declare function getInputType(field: PrismaField): string;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma Schema Parser
|
|
3
|
+
* Extracts model information from Prisma schema files
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
const SCALAR_TYPES = ['String', 'Int', 'Float', 'Boolean', 'DateTime', 'Json', 'Bytes', 'Decimal', 'BigInt'];
|
|
7
|
+
export function parsePrismaSchema(schemaPath) {
|
|
8
|
+
const content = readFileSync(schemaPath, 'utf-8');
|
|
9
|
+
return parseSchemaContent(content);
|
|
10
|
+
}
|
|
11
|
+
export function parseSchemaContent(content) {
|
|
12
|
+
const models = [];
|
|
13
|
+
const enums = new Map();
|
|
14
|
+
// Parse enums
|
|
15
|
+
const enumRegex = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
16
|
+
let enumMatch;
|
|
17
|
+
while ((enumMatch = enumRegex.exec(content)) !== null) {
|
|
18
|
+
const enumName = enumMatch[1];
|
|
19
|
+
const enumValues = enumMatch[2]
|
|
20
|
+
.split('\n')
|
|
21
|
+
.map(line => line.trim())
|
|
22
|
+
.filter(line => line && !line.startsWith('//'));
|
|
23
|
+
enums.set(enumName, enumValues);
|
|
24
|
+
}
|
|
25
|
+
// Parse models
|
|
26
|
+
const modelRegex = /(?:\/\/\/\s*(.+)\n)?model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
27
|
+
let modelMatch;
|
|
28
|
+
while ((modelMatch = modelRegex.exec(content)) !== null) {
|
|
29
|
+
const documentation = modelMatch[1]?.trim();
|
|
30
|
+
const modelName = modelMatch[2];
|
|
31
|
+
const modelBody = modelMatch[3];
|
|
32
|
+
const fields = parseModelFields(modelBody, enums);
|
|
33
|
+
const primaryKey = fields.find(f => f.isId)?.name || 'id';
|
|
34
|
+
models.push({
|
|
35
|
+
name: modelName,
|
|
36
|
+
fields,
|
|
37
|
+
documentation,
|
|
38
|
+
primaryKey
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return { models, enums };
|
|
42
|
+
}
|
|
43
|
+
function parseModelFields(modelBody, enums) {
|
|
44
|
+
const fields = [];
|
|
45
|
+
const lines = modelBody.split('\n');
|
|
46
|
+
let currentDoc;
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
const trimmed = line.trim();
|
|
49
|
+
// Skip empty lines and block-level attributes
|
|
50
|
+
if (!trimmed || trimmed.startsWith('@@'))
|
|
51
|
+
continue;
|
|
52
|
+
// Capture documentation comments
|
|
53
|
+
if (trimmed.startsWith('///')) {
|
|
54
|
+
currentDoc = trimmed.slice(3).trim();
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// Skip regular comments
|
|
58
|
+
if (trimmed.startsWith('//'))
|
|
59
|
+
continue;
|
|
60
|
+
// Parse field
|
|
61
|
+
const field = parseFieldLine(trimmed, enums, currentDoc);
|
|
62
|
+
if (field) {
|
|
63
|
+
fields.push(field);
|
|
64
|
+
}
|
|
65
|
+
currentDoc = undefined;
|
|
66
|
+
}
|
|
67
|
+
return fields;
|
|
68
|
+
}
|
|
69
|
+
function parseFieldLine(line, enums, documentation) {
|
|
70
|
+
// Match: fieldName Type? @attributes
|
|
71
|
+
const fieldMatch = line.match(/^(\w+)\s+(\w+)(\[\])?(\?)?(.*)$/);
|
|
72
|
+
if (!fieldMatch)
|
|
73
|
+
return null;
|
|
74
|
+
const [, name, rawType, listMarker, optionalMarker, attributes] = fieldMatch;
|
|
75
|
+
const type = rawType;
|
|
76
|
+
const isList = !!listMarker;
|
|
77
|
+
const isRequired = !optionalMarker && !isList;
|
|
78
|
+
// Parse attributes
|
|
79
|
+
const isId = /@id\b/.test(attributes);
|
|
80
|
+
const isUnique = /@unique\b/.test(attributes);
|
|
81
|
+
const isUpdatedAt = /@updatedAt\b/.test(attributes);
|
|
82
|
+
const hasDefault = /@default\b/.test(attributes);
|
|
83
|
+
// Detect createdAt pattern
|
|
84
|
+
const isCreatedAt = name.toLowerCase() === 'createdat' ||
|
|
85
|
+
(type === 'DateTime' && /@default\s*\(\s*now\s*\(\s*\)\s*\)/.test(attributes));
|
|
86
|
+
// Parse default value
|
|
87
|
+
let defaultValue;
|
|
88
|
+
const defaultMatch = attributes.match(/@default\s*\(([^)]+)\)/);
|
|
89
|
+
if (defaultMatch) {
|
|
90
|
+
defaultValue = defaultMatch[1].trim();
|
|
91
|
+
}
|
|
92
|
+
// Parse relation
|
|
93
|
+
let relation;
|
|
94
|
+
const relationMatch = attributes.match(/@relation\s*\(([^)]*)\)/);
|
|
95
|
+
if (relationMatch || (!SCALAR_TYPES.includes(type) && !enums.has(type))) {
|
|
96
|
+
relation = {
|
|
97
|
+
model: type,
|
|
98
|
+
};
|
|
99
|
+
if (relationMatch) {
|
|
100
|
+
const relContent = relationMatch[1];
|
|
101
|
+
// Parse relation name
|
|
102
|
+
const nameMatch = relContent.match(/name:\s*"([^"]+)"/);
|
|
103
|
+
if (nameMatch)
|
|
104
|
+
relation.name = nameMatch[1];
|
|
105
|
+
// Parse fields
|
|
106
|
+
const fieldsMatch = relContent.match(/fields:\s*\[([^\]]+)\]/);
|
|
107
|
+
if (fieldsMatch) {
|
|
108
|
+
relation.fields = fieldsMatch[1].split(',').map(f => f.trim());
|
|
109
|
+
}
|
|
110
|
+
// Parse references
|
|
111
|
+
const refsMatch = relContent.match(/references:\s*\[([^\]]+)\]/);
|
|
112
|
+
if (refsMatch) {
|
|
113
|
+
relation.references = refsMatch[1].split(',').map(r => r.trim());
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
name,
|
|
119
|
+
type,
|
|
120
|
+
isRequired,
|
|
121
|
+
isList,
|
|
122
|
+
isUnique,
|
|
123
|
+
isId,
|
|
124
|
+
isUpdatedAt,
|
|
125
|
+
isCreatedAt,
|
|
126
|
+
hasDefault,
|
|
127
|
+
defaultValue,
|
|
128
|
+
relation,
|
|
129
|
+
documentation
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get display fields for a model (fields suitable for list view)
|
|
134
|
+
*/
|
|
135
|
+
export function getDisplayFields(model) {
|
|
136
|
+
return model.fields.filter(f => !f.relation?.fields && // Skip relation foreign keys shown separately
|
|
137
|
+
!f.isList && // Skip array fields
|
|
138
|
+
!['password', 'hashedPassword', 'hash', 'secret'].some(hidden => f.name.toLowerCase().includes(hidden)));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get editable fields for a model (fields that can be edited in forms)
|
|
142
|
+
*/
|
|
143
|
+
export function getEditableFields(model) {
|
|
144
|
+
return model.fields.filter(f => !f.isId &&
|
|
145
|
+
!f.isCreatedAt &&
|
|
146
|
+
!f.isUpdatedAt &&
|
|
147
|
+
!f.isList &&
|
|
148
|
+
!f.relation?.references // Skip the "other side" of relations
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get a human-readable label from a field name
|
|
153
|
+
*/
|
|
154
|
+
export function fieldToLabel(fieldName) {
|
|
155
|
+
return fieldName
|
|
156
|
+
.replace(/([A-Z])/g, ' $1')
|
|
157
|
+
.replace(/^./, str => str.toUpperCase())
|
|
158
|
+
.trim();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Determine the input type for a Prisma field
|
|
162
|
+
*/
|
|
163
|
+
export function getInputType(field) {
|
|
164
|
+
if (field.relation)
|
|
165
|
+
return 'relation';
|
|
166
|
+
switch (field.type) {
|
|
167
|
+
case 'String':
|
|
168
|
+
if (field.name.toLowerCase().includes('email'))
|
|
169
|
+
return 'email';
|
|
170
|
+
if (field.name.toLowerCase().includes('password'))
|
|
171
|
+
return 'password';
|
|
172
|
+
if (field.name.toLowerCase().includes('url'))
|
|
173
|
+
return 'url';
|
|
174
|
+
if (field.name.toLowerCase().includes('description') ||
|
|
175
|
+
field.name.toLowerCase().includes('content') ||
|
|
176
|
+
field.name.toLowerCase().includes('bio'))
|
|
177
|
+
return 'textarea';
|
|
178
|
+
return 'text';
|
|
179
|
+
case 'Int':
|
|
180
|
+
case 'Float':
|
|
181
|
+
case 'Decimal':
|
|
182
|
+
case 'BigInt':
|
|
183
|
+
return 'number';
|
|
184
|
+
case 'Boolean':
|
|
185
|
+
return 'checkbox';
|
|
186
|
+
case 'DateTime':
|
|
187
|
+
return 'datetime';
|
|
188
|
+
case 'Json':
|
|
189
|
+
return 'json';
|
|
190
|
+
default:
|
|
191
|
+
return 'text';
|
|
192
|
+
}
|
|
193
|
+
}
|