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
package/dist/admin.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvelteKit Admin - Core Admin Factory
|
|
3
|
+
* Creates all the necessary handlers for the admin panel
|
|
4
|
+
*/
|
|
5
|
+
import { parsePrismaSchema, getDisplayFields, getEditableFields, getInputType, fieldToLabel } from './server/introspection/parser.js';
|
|
6
|
+
import { createListOperation, createGetOperation, createCreateOperation, createUpdateOperation, createDeleteOperation } from './server/crud/operations.js';
|
|
7
|
+
import { defaultAdminCheck } from './server/auth/guard.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create admin context with all necessary data
|
|
10
|
+
*/
|
|
11
|
+
export function createAdmin(config) {
|
|
12
|
+
// Parse schema if not provided
|
|
13
|
+
let schema = config.schema;
|
|
14
|
+
if (!schema && config.schemaPath) {
|
|
15
|
+
schema = parsePrismaSchema(config.schemaPath);
|
|
16
|
+
}
|
|
17
|
+
if (!schema) {
|
|
18
|
+
throw new Error('Either schema or schemaPath must be provided');
|
|
19
|
+
}
|
|
20
|
+
// Filter excluded models
|
|
21
|
+
const exclude = config.exclude || [];
|
|
22
|
+
const models = schema.models.filter(m => !exclude.includes(m.name));
|
|
23
|
+
return {
|
|
24
|
+
config,
|
|
25
|
+
schema,
|
|
26
|
+
models,
|
|
27
|
+
getModel: (name) => models.find(m => m.name.toLowerCase() === name.toLowerCase())
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Layout data loader - provides models list and config to layout
|
|
32
|
+
*/
|
|
33
|
+
export function createLayoutLoad(ctx) {
|
|
34
|
+
return async ({ locals }) => {
|
|
35
|
+
const user = locals.user;
|
|
36
|
+
return {
|
|
37
|
+
models: ctx.models.map(m => ({
|
|
38
|
+
name: m.name,
|
|
39
|
+
label: ctx.config.models?.[m.name]?.label || m.name
|
|
40
|
+
})),
|
|
41
|
+
user: user ? { name: user.name, email: user.email } : undefined,
|
|
42
|
+
config: {
|
|
43
|
+
basePath: ctx.config.basePath || '/admin',
|
|
44
|
+
branding: {
|
|
45
|
+
title: ctx.config.branding?.title || 'Admin',
|
|
46
|
+
logo: ctx.config.branding?.logo,
|
|
47
|
+
primaryColor: ctx.config.branding?.primaryColor || '#6366f1'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Dashboard data loader - provides model counts
|
|
55
|
+
*/
|
|
56
|
+
export function createDashboardLoad(ctx) {
|
|
57
|
+
return async () => {
|
|
58
|
+
const modelCounts = await Promise.all(ctx.models.map(async (model) => {
|
|
59
|
+
const modelKey = model.name.charAt(0).toLowerCase() + model.name.slice(1);
|
|
60
|
+
const prismaModel = ctx.config.prisma[modelKey];
|
|
61
|
+
let count = 0;
|
|
62
|
+
if (prismaModel?.count) {
|
|
63
|
+
try {
|
|
64
|
+
count = await prismaModel.count();
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
// Model might not exist in DB yet
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
name: model.name,
|
|
72
|
+
label: ctx.config.models?.[model.name]?.label || model.name,
|
|
73
|
+
count
|
|
74
|
+
};
|
|
75
|
+
}));
|
|
76
|
+
const totalRecords = modelCounts.reduce((sum, m) => sum + m.count, 0);
|
|
77
|
+
return {
|
|
78
|
+
models: modelCounts,
|
|
79
|
+
stats: {
|
|
80
|
+
totalRecords,
|
|
81
|
+
modelsCount: ctx.models.length
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Model list data loader
|
|
88
|
+
*/
|
|
89
|
+
export function createModelListLoad(ctx) {
|
|
90
|
+
return async ({ params, url }) => {
|
|
91
|
+
const model = ctx.getModel(params.model);
|
|
92
|
+
if (!model) {
|
|
93
|
+
throw new Error(`Model ${params.model} not found`);
|
|
94
|
+
}
|
|
95
|
+
const modelConfig = ctx.config.models?.[model.name] || {};
|
|
96
|
+
const hidden = modelConfig.hidden || [];
|
|
97
|
+
// Parse query params
|
|
98
|
+
const page = parseInt(url.searchParams.get('page') || '1');
|
|
99
|
+
const perPage = parseInt(url.searchParams.get('perPage') || '20');
|
|
100
|
+
const search = url.searchParams.get('search') || '';
|
|
101
|
+
const orderBy = url.searchParams.get('orderBy') || model.primaryKey || 'id';
|
|
102
|
+
const orderDir = (url.searchParams.get('orderDir') || 'desc');
|
|
103
|
+
// Get display fields
|
|
104
|
+
let displayFields = getDisplayFields(model).filter(f => !hidden.includes(f.name));
|
|
105
|
+
if (modelConfig.listFields?.length) {
|
|
106
|
+
displayFields = displayFields.filter(f => modelConfig.listFields.includes(f.name));
|
|
107
|
+
}
|
|
108
|
+
// Execute list query
|
|
109
|
+
const listOp = createListOperation(model);
|
|
110
|
+
const result = await listOp.execute(ctx.config.prisma, {
|
|
111
|
+
page,
|
|
112
|
+
perPage,
|
|
113
|
+
search,
|
|
114
|
+
orderBy,
|
|
115
|
+
orderDir
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
model: {
|
|
119
|
+
name: model.name,
|
|
120
|
+
label: modelConfig.label || model.name,
|
|
121
|
+
fields: displayFields.map(f => ({
|
|
122
|
+
name: f.name,
|
|
123
|
+
type: f.type,
|
|
124
|
+
label: fieldToLabel(f.name)
|
|
125
|
+
})),
|
|
126
|
+
primaryKey: model.primaryKey || 'id'
|
|
127
|
+
},
|
|
128
|
+
items: result.items,
|
|
129
|
+
total: result.total,
|
|
130
|
+
page: result.page,
|
|
131
|
+
perPage: result.perPage,
|
|
132
|
+
orderBy,
|
|
133
|
+
orderDir,
|
|
134
|
+
search,
|
|
135
|
+
config: {
|
|
136
|
+
basePath: ctx.config.basePath || '/admin',
|
|
137
|
+
hidden,
|
|
138
|
+
listFields: modelConfig.listFields
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Model create page loader
|
|
145
|
+
*/
|
|
146
|
+
export function createModelNewLoad(ctx) {
|
|
147
|
+
return async ({ params }) => {
|
|
148
|
+
const model = ctx.getModel(params.model);
|
|
149
|
+
if (!model) {
|
|
150
|
+
throw new Error(`Model ${params.model} not found`);
|
|
151
|
+
}
|
|
152
|
+
const modelConfig = ctx.config.models?.[model.name] || {};
|
|
153
|
+
const hidden = modelConfig.hidden || [];
|
|
154
|
+
const readonly = modelConfig.readonly || [];
|
|
155
|
+
const editableFields = getEditableFields(model).filter(f => !hidden.includes(f.name));
|
|
156
|
+
// Load relation options
|
|
157
|
+
const relationOptions = {};
|
|
158
|
+
for (const field of editableFields) {
|
|
159
|
+
if (field.relation) {
|
|
160
|
+
const relatedModel = ctx.getModel(field.relation.model);
|
|
161
|
+
if (relatedModel) {
|
|
162
|
+
const relKey = field.relation.model.charAt(0).toLowerCase() + field.relation.model.slice(1);
|
|
163
|
+
const relPrisma = ctx.config.prisma[relKey];
|
|
164
|
+
if (relPrisma?.findMany) {
|
|
165
|
+
try {
|
|
166
|
+
const items = await relPrisma.findMany({ take: 100 });
|
|
167
|
+
relationOptions[field.name] = items.map((item) => ({
|
|
168
|
+
id: item.id,
|
|
169
|
+
label: item.name || item.title || item.email || String(item.id)
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
// Ignore errors
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
model: {
|
|
181
|
+
name: model.name,
|
|
182
|
+
label: modelConfig.label || model.name,
|
|
183
|
+
fields: editableFields.map(f => ({
|
|
184
|
+
name: f.name,
|
|
185
|
+
type: f.type,
|
|
186
|
+
required: f.isRequired && !f.hasDefault,
|
|
187
|
+
label: fieldToLabel(f.name)
|
|
188
|
+
}))
|
|
189
|
+
},
|
|
190
|
+
config: {
|
|
191
|
+
basePath: ctx.config.basePath || '/admin',
|
|
192
|
+
hidden,
|
|
193
|
+
readonly
|
|
194
|
+
},
|
|
195
|
+
relationOptions
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Model create action
|
|
201
|
+
*/
|
|
202
|
+
export function createModelNewAction(ctx) {
|
|
203
|
+
return async ({ params, request }) => {
|
|
204
|
+
const model = ctx.getModel(params.model);
|
|
205
|
+
if (!model) {
|
|
206
|
+
throw new Error(`Model ${params.model} not found`);
|
|
207
|
+
}
|
|
208
|
+
const formData = await request.formData();
|
|
209
|
+
const data = {};
|
|
210
|
+
formData.forEach((value, key) => {
|
|
211
|
+
data[key] = value;
|
|
212
|
+
});
|
|
213
|
+
const createOp = createCreateOperation(model);
|
|
214
|
+
try {
|
|
215
|
+
await createOp.execute(ctx.config.prisma, data);
|
|
216
|
+
return { success: true };
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return {
|
|
220
|
+
success: false,
|
|
221
|
+
error: error.message,
|
|
222
|
+
fieldErrors: {}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Model edit page loader
|
|
229
|
+
*/
|
|
230
|
+
export function createModelEditLoad(ctx) {
|
|
231
|
+
return async ({ params }) => {
|
|
232
|
+
const model = ctx.getModel(params.model);
|
|
233
|
+
if (!model) {
|
|
234
|
+
throw new Error(`Model ${params.model} not found`);
|
|
235
|
+
}
|
|
236
|
+
const modelConfig = ctx.config.models?.[model.name] || {};
|
|
237
|
+
const hidden = modelConfig.hidden || [];
|
|
238
|
+
const readonly = modelConfig.readonly || [];
|
|
239
|
+
// Get the record
|
|
240
|
+
const getOp = createGetOperation(model);
|
|
241
|
+
const item = await getOp.execute(ctx.config.prisma, params.id);
|
|
242
|
+
if (!item) {
|
|
243
|
+
throw new Error(`Record not found`);
|
|
244
|
+
}
|
|
245
|
+
const allFields = model.fields.filter(f => !hidden.includes(f.name));
|
|
246
|
+
// Load relation options for editable relation fields
|
|
247
|
+
const relationOptions = {};
|
|
248
|
+
for (const field of allFields) {
|
|
249
|
+
if (field.relation && !field.isList) {
|
|
250
|
+
const relatedModel = ctx.getModel(field.relation.model);
|
|
251
|
+
if (relatedModel) {
|
|
252
|
+
const relKey = field.relation.model.charAt(0).toLowerCase() + field.relation.model.slice(1);
|
|
253
|
+
const relPrisma = ctx.config.prisma[relKey];
|
|
254
|
+
if (relPrisma?.findMany) {
|
|
255
|
+
try {
|
|
256
|
+
const items = await relPrisma.findMany({ take: 100 });
|
|
257
|
+
relationOptions[field.name] = items.map((item) => ({
|
|
258
|
+
id: item.id,
|
|
259
|
+
label: item.name || item.title || item.email || String(item.id)
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
// Ignore errors
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
model: {
|
|
271
|
+
name: model.name,
|
|
272
|
+
label: modelConfig.label || model.name,
|
|
273
|
+
primaryKey: model.primaryKey || 'id',
|
|
274
|
+
fields: allFields.map(f => ({
|
|
275
|
+
name: f.name,
|
|
276
|
+
type: f.type,
|
|
277
|
+
required: f.isRequired,
|
|
278
|
+
label: fieldToLabel(f.name)
|
|
279
|
+
}))
|
|
280
|
+
},
|
|
281
|
+
item,
|
|
282
|
+
config: {
|
|
283
|
+
basePath: ctx.config.basePath || '/admin',
|
|
284
|
+
hidden,
|
|
285
|
+
readonly
|
|
286
|
+
},
|
|
287
|
+
relationOptions
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Model update action
|
|
293
|
+
*/
|
|
294
|
+
export function createModelEditAction(ctx) {
|
|
295
|
+
return async ({ params, request }) => {
|
|
296
|
+
const model = ctx.getModel(params.model);
|
|
297
|
+
if (!model) {
|
|
298
|
+
throw new Error(`Model ${params.model} not found`);
|
|
299
|
+
}
|
|
300
|
+
const formData = await request.formData();
|
|
301
|
+
const data = {};
|
|
302
|
+
formData.forEach((value, key) => {
|
|
303
|
+
data[key] = value;
|
|
304
|
+
});
|
|
305
|
+
const updateOp = createUpdateOperation(model);
|
|
306
|
+
try {
|
|
307
|
+
await updateOp.execute(ctx.config.prisma, params.id, data);
|
|
308
|
+
return { success: true };
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
return {
|
|
312
|
+
success: false,
|
|
313
|
+
error: error.message
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Model delete action
|
|
320
|
+
*/
|
|
321
|
+
export function createModelDeleteAction(ctx) {
|
|
322
|
+
return async ({ params, request }) => {
|
|
323
|
+
const model = ctx.getModel(params.model);
|
|
324
|
+
if (!model) {
|
|
325
|
+
throw new Error(`Model ${params.model} not found`);
|
|
326
|
+
}
|
|
327
|
+
const formData = await request.formData();
|
|
328
|
+
const id = formData.get('id');
|
|
329
|
+
if (!id) {
|
|
330
|
+
throw new Error('Missing ID');
|
|
331
|
+
}
|
|
332
|
+
const deleteOp = createDeleteOperation(model);
|
|
333
|
+
try {
|
|
334
|
+
await deleteOp.execute(ctx.config.prisma, String(id));
|
|
335
|
+
return { success: true };
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
return {
|
|
339
|
+
success: false,
|
|
340
|
+
error: error.message
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Auth guard hook
|
|
347
|
+
*/
|
|
348
|
+
export function createAdminGuard(ctx) {
|
|
349
|
+
const basePath = ctx.config.basePath || '/admin';
|
|
350
|
+
return async ({ event, resolve }) => {
|
|
351
|
+
if (!event.url.pathname.startsWith(basePath)) {
|
|
352
|
+
return resolve(event);
|
|
353
|
+
}
|
|
354
|
+
const user = event.locals.user;
|
|
355
|
+
if (!user) {
|
|
356
|
+
return new Response(null, {
|
|
357
|
+
status: 302,
|
|
358
|
+
headers: { Location: '/login' }
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const checkFn = ctx.config.checkAdmin ||
|
|
362
|
+
((u) => defaultAdminCheck(u, ctx.config.adminRole || 'admin'));
|
|
363
|
+
const isAdmin = await checkFn(user);
|
|
364
|
+
if (!isAdmin) {
|
|
365
|
+
return new Response('Forbidden', { status: 403 });
|
|
366
|
+
}
|
|
367
|
+
return resolve(event);
|
|
368
|
+
};
|
|
369
|
+
}
|