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/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
+ }