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.
@@ -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
+ }