sveltekit-admin 0.5.3 → 0.8.1

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.
Files changed (71) hide show
  1. package/README.md +89 -1
  2. package/dist/index.d.ts +6 -1
  3. package/dist/index.js +2 -1
  4. package/dist/server/adapters/drizzle/dataAdapter.d.ts +11 -0
  5. package/dist/server/adapters/drizzle/dataAdapter.js +328 -0
  6. package/dist/server/adapters/drizzle/filterCompiler.d.ts +8 -0
  7. package/dist/server/adapters/drizzle/filterCompiler.js +61 -0
  8. package/dist/server/adapters/drizzle/index.d.ts +22 -0
  9. package/dist/server/adapters/drizzle/index.js +27 -0
  10. package/dist/server/adapters/drizzle/inspect.d.ts +23 -0
  11. package/dist/server/adapters/drizzle/inspect.js +220 -0
  12. package/dist/server/adapters/drizzle/introspector.d.ts +3 -0
  13. package/dist/server/adapters/drizzle/introspector.js +3 -0
  14. package/dist/server/adapters/filter.d.ts +12 -0
  15. package/dist/server/adapters/filter.js +72 -0
  16. package/dist/server/adapters/prisma/dataAdapter.d.ts +13 -0
  17. package/dist/server/adapters/prisma/dataAdapter.js +138 -0
  18. package/dist/server/adapters/prisma/filterCompiler.d.ts +9 -0
  19. package/dist/server/adapters/prisma/filterCompiler.js +42 -0
  20. package/dist/server/adapters/prisma/handler.d.ts +14 -0
  21. package/dist/server/adapters/prisma/handler.js +37 -0
  22. package/dist/server/adapters/prisma/index.d.ts +28 -0
  23. package/dist/server/adapters/prisma/index.js +32 -0
  24. package/dist/server/adapters/prisma/introspector.d.ts +4 -0
  25. package/dist/server/adapters/prisma/introspector.js +9 -0
  26. package/dist/server/adapters/retry.d.ts +27 -0
  27. package/dist/server/adapters/retry.js +53 -0
  28. package/dist/server/adapters/types.d.ts +86 -0
  29. package/dist/server/adapters/types.js +1 -0
  30. package/dist/server/audit.d.ts +65 -0
  31. package/dist/server/audit.js +106 -0
  32. package/dist/server/csrf.d.ts +33 -0
  33. package/dist/server/csrf.js +55 -0
  34. package/dist/server/data.d.ts +0 -8
  35. package/dist/server/data.js +0 -41
  36. package/dist/server/errors.d.ts +47 -0
  37. package/dist/server/errors.js +90 -0
  38. package/dist/server/handler.d.ts +63 -16
  39. package/dist/server/handler.js +212 -535
  40. package/dist/server/introspection/parser.d.ts +4 -40
  41. package/dist/server/introspection/relations.d.ts +1 -1
  42. package/dist/server/introspection/relations.js +1 -1
  43. package/dist/server/mutations.d.ts +9 -0
  44. package/dist/server/mutations.js +295 -0
  45. package/dist/server/plugin.d.ts +47 -0
  46. package/dist/server/plugin.js +1 -0
  47. package/dist/server/pluginAccess.d.ts +7 -0
  48. package/dist/server/pluginAccess.js +79 -0
  49. package/dist/server/pluginRegistry.d.ts +12 -0
  50. package/dist/server/pluginRegistry.js +72 -0
  51. package/dist/server/query/listQuery.d.ts +6 -12
  52. package/dist/server/query/listQuery.js +31 -53
  53. package/dist/server/relationLoaders.d.ts +42 -0
  54. package/dist/server/relationLoaders.js +188 -0
  55. package/dist/server/router.d.ts +10 -0
  56. package/dist/server/router.js +42 -19
  57. package/dist/server/runtime.d.ts +46 -0
  58. package/dist/server/runtime.js +210 -0
  59. package/dist/server/search.d.ts +14 -0
  60. package/dist/server/search.js +78 -0
  61. package/dist/server/types/schema.d.ts +40 -0
  62. package/dist/server/types/schema.js +8 -0
  63. package/dist/server/views/Form.svelte +27 -3
  64. package/dist/server/views/Form.svelte.d.ts +2 -1
  65. package/dist/server/views/Layout.svelte +27 -2
  66. package/dist/server/views/Layout.svelte.d.ts +2 -0
  67. package/dist/server/views/List.svelte +28 -3
  68. package/dist/server/views/List.svelte.d.ts +2 -2
  69. package/dist/server/views/RelationCheckboxes.svelte +1 -1
  70. package/dist/server/views/types.d.ts +8 -0
  71. package/package.json +40 -17
@@ -0,0 +1,220 @@
1
+ import { createTableRelationsHelpers, extractTablesRelationalConfig, is, Many, One, Table } from 'drizzle-orm';
2
+ import { MySqlTable } from 'drizzle-orm/mysql-core';
3
+ import { PgTable } from 'drizzle-orm/pg-core';
4
+ import { SQLiteTable } from 'drizzle-orm/sqlite-core';
5
+ function inferDialect(tables) {
6
+ const dialects = new Set();
7
+ for (const table of tables) {
8
+ if (is(table, PgTable))
9
+ dialects.add('postgresql');
10
+ else if (is(table, MySqlTable))
11
+ dialects.add('mysql');
12
+ else if (is(table, SQLiteTable))
13
+ dialects.add('sqlite');
14
+ }
15
+ if (dialects.size > 1) {
16
+ throw new Error('[sveltekit-admin] createDrizzleAdapter: mixed table dialects in schema (pgTable / mysqlTable / sqliteTable)');
17
+ }
18
+ if (dialects.size === 0)
19
+ return 'sqlite';
20
+ return [...dialects][0];
21
+ }
22
+ export function mapColumnType(column, enumTypeName) {
23
+ const enumValues = column.enumValues;
24
+ if (enumValues && enumValues.length > 0) {
25
+ return {
26
+ type: (enumTypeName ?? column.columnType.replace(/^(Pg|MySql|SQLite)/, '')) || 'String',
27
+ isEnum: true
28
+ };
29
+ }
30
+ const dataType = column.dataType;
31
+ const columnType = column.columnType;
32
+ if (dataType === 'date' || /Timestamp/i.test(columnType)) {
33
+ return { type: 'DateTime', isEnum: false };
34
+ }
35
+ if (dataType === 'boolean')
36
+ return { type: 'Boolean', isEnum: false };
37
+ if (dataType === 'json')
38
+ return { type: 'Json', isEnum: false };
39
+ if (dataType === 'bigint')
40
+ return { type: 'BigInt', isEnum: false };
41
+ if (dataType === 'buffer')
42
+ return { type: 'Bytes', isEnum: false };
43
+ if (dataType === 'string')
44
+ return { type: 'String', isEnum: false };
45
+ if (dataType === 'number') {
46
+ if (/Numeric|Decimal/i.test(columnType))
47
+ return { type: 'Decimal', isEnum: false };
48
+ if (/Real|Float|Double/i.test(columnType))
49
+ return { type: 'Float', isEnum: false };
50
+ return { type: 'Int', isEnum: false };
51
+ }
52
+ return { type: 'String', isEnum: false };
53
+ }
54
+ function timestampFlag(jsName, type) {
55
+ if (type !== 'DateTime')
56
+ return { isCreatedAt: false, isUpdatedAt: false };
57
+ const normalizedName = jsName.toLowerCase().replace(/_/g, '');
58
+ return {
59
+ isCreatedAt: normalizedName === 'createdat',
60
+ isUpdatedAt: normalizedName === 'updatedat'
61
+ };
62
+ }
63
+ export function inspectDrizzleSchema(schemaObj, dialectOverride) {
64
+ const rawTables = [];
65
+ const tables = {};
66
+ for (const [key, value] of Object.entries(schemaObj)) {
67
+ if (is(value, Table)) {
68
+ rawTables.push(value);
69
+ tables[key] = value;
70
+ }
71
+ }
72
+ const inferred = inferDialect(rawTables);
73
+ if (dialectOverride && rawTables.length > 0 && inferred !== dialectOverride) {
74
+ throw new Error(`[sveltekit-admin] createDrizzleAdapter: dialect override '${dialectOverride}' does not match inferred '${inferred}'`);
75
+ }
76
+ const resolved = dialectOverride ?? inferred;
77
+ const { tables: relationalTables } = extractTablesRelationalConfig(schemaObj, createTableRelationsHelpers);
78
+ const enums = new Map();
79
+ const models = [];
80
+ const modelByName = new Map();
81
+ const relationsByModel = new Map();
82
+ for (const [tsName, config] of Object.entries(relationalTables)) {
83
+ const fields = [];
84
+ for (const [jsName, column] of Object.entries(config.columns)) {
85
+ const enumName = column.enum?.enumName;
86
+ const syntheticEnumName = enumName ?? `DrizzleEnum:${tsName.length}:${tsName}:${jsName.length}:${jsName}`;
87
+ const { type, isEnum } = mapColumnType(column, syntheticEnumName);
88
+ if (isEnum) {
89
+ const enumValues = column.enumValues;
90
+ enums.set(type, enumValues);
91
+ }
92
+ const { isCreatedAt, isUpdatedAt } = timestampFlag(jsName, type);
93
+ fields.push({
94
+ name: jsName,
95
+ type,
96
+ isRequired: Boolean(column.notNull),
97
+ isList: false,
98
+ isUnique: Boolean(column.isUnique),
99
+ isId: Boolean(column.primary),
100
+ isUpdatedAt,
101
+ isCreatedAt,
102
+ hasDefault: Boolean(column.hasDefault),
103
+ isEnum: isEnum || undefined
104
+ });
105
+ }
106
+ const model = { name: tsName, fields };
107
+ models.push(model);
108
+ modelByName.set(tsName, model);
109
+ }
110
+ for (const [tsName, config] of Object.entries(relationalTables)) {
111
+ const inspectedRelations = [];
112
+ for (const [name, relation] of Object.entries(config.relations)) {
113
+ const targetTsName = Object.entries(tables).find(([, table]) => table === relation.referencedTable)[0];
114
+ const relationFields = is(relation, One) && relation.config?.fields
115
+ ? relation.config.fields.map((field) => Object.entries(config.columns).find(([, column]) => column === field)[0])
116
+ : undefined;
117
+ modelByName.get(tsName).fields.push({
118
+ name,
119
+ type: targetTsName,
120
+ isRequired: false,
121
+ isList: is(relation, Many),
122
+ isUnique: false,
123
+ isId: false,
124
+ isUpdatedAt: false,
125
+ isCreatedAt: false,
126
+ hasDefault: false,
127
+ relation: {
128
+ model: targetTsName,
129
+ name: relation.relationName,
130
+ fields: relationFields
131
+ }
132
+ });
133
+ inspectedRelations.push({ name, relation, targetTsName });
134
+ }
135
+ relationsByModel.set(tsName, inspectedRelations);
136
+ }
137
+ const m2m = new Map();
138
+ for (const [pivotTsName, pivotRelations] of relationsByModel) {
139
+ const owning = pivotRelations.filter(({ relation }) => is(relation, One) && Boolean(relation.config?.fields));
140
+ if (owning.length !== 2 || owning[0].targetTsName === owning[1].targetTsName)
141
+ continue;
142
+ const [a, b] = owning;
143
+ const aMany = relationsByModel
144
+ .get(a.targetTsName)
145
+ .find(({ relation, targetTsName }) => is(relation, Many) && targetTsName === pivotTsName);
146
+ const bMany = relationsByModel
147
+ .get(b.targetTsName)
148
+ .find(({ relation, targetTsName }) => is(relation, Many) && targetTsName === pivotTsName);
149
+ if (!aMany || !bMany)
150
+ continue;
151
+ const pivotModel = modelByName.get(pivotTsName);
152
+ const aFk = a.relation.config.fields[0];
153
+ const bFk = b.relation.config.fields[0];
154
+ const aFkJs = Object.entries(relationalTables[pivotTsName].columns).find(([, column]) => column === aFk)[0];
155
+ const bFkJs = Object.entries(relationalTables[pivotTsName].columns).find(([, column]) => column === bFk)[0];
156
+ const businessColumns = pivotModel.fields.filter((field) => !field.relation &&
157
+ !field.isId &&
158
+ field.name !== aFkJs &&
159
+ field.name !== bFkJs &&
160
+ !field.isCreatedAt &&
161
+ !field.isUpdatedAt);
162
+ if (businessColumns.length > 0)
163
+ continue;
164
+ const aModel = modelByName.get(a.targetTsName);
165
+ const bModel = modelByName.get(b.targetTsName);
166
+ if (aModel.fields.some((field) => field.name === b.targetTsName) ||
167
+ bModel.fields.some((field) => field.name === a.targetTsName)) {
168
+ continue;
169
+ }
170
+ pivotModel.isPivotTable = true;
171
+ aModel.fields = aModel.fields.filter((field) => field.name !== aMany.name);
172
+ bModel.fields = bModel.fields.filter((field) => field.name !== bMany.name);
173
+ aModel.fields.push({
174
+ name: b.targetTsName,
175
+ type: b.targetTsName,
176
+ isRequired: false,
177
+ isList: true,
178
+ isUnique: false,
179
+ isId: false,
180
+ isUpdatedAt: false,
181
+ isCreatedAt: false,
182
+ hasDefault: false,
183
+ relation: { model: b.targetTsName }
184
+ });
185
+ bModel.fields.push({
186
+ name: a.targetTsName,
187
+ type: a.targetTsName,
188
+ isRequired: false,
189
+ isList: true,
190
+ isUnique: false,
191
+ isId: false,
192
+ isUpdatedAt: false,
193
+ isCreatedAt: false,
194
+ hasDefault: false,
195
+ relation: { model: a.targetTsName }
196
+ });
197
+ m2m.set(`${a.targetTsName}.${b.targetTsName}`, {
198
+ pivot: tables[pivotTsName],
199
+ selfColumn: aFk,
200
+ otherColumn: bFk,
201
+ selfKey: aFkJs,
202
+ otherKey: bFkJs,
203
+ targetTsName: b.targetTsName
204
+ });
205
+ m2m.set(`${b.targetTsName}.${a.targetTsName}`, {
206
+ pivot: tables[pivotTsName],
207
+ selfColumn: bFk,
208
+ otherColumn: aFk,
209
+ selfKey: bFkJs,
210
+ otherKey: aFkJs,
211
+ targetTsName: a.targetTsName
212
+ });
213
+ }
214
+ return {
215
+ schema: { models, enums, provider: resolved },
216
+ tables,
217
+ m2m,
218
+ dialect: resolved
219
+ };
220
+ }
@@ -0,0 +1,3 @@
1
+ import type { SchemaIntrospector } from "../types.js";
2
+ import type { Schema } from "../../types/schema.js";
3
+ export declare function createDrizzleIntrospector(schema: Schema): SchemaIntrospector;
@@ -0,0 +1,3 @@
1
+ export function createDrizzleIntrospector(schema) {
2
+ return { introspect: () => schema };
3
+ }
@@ -0,0 +1,12 @@
1
+ import type { CompositeFilter, Filter, LeafFilter } from './types.js';
2
+ export declare const OPAQUE_FILTER_ERROR = "nested Prisma `where` is not supported by the Drizzle adapter; return a Filter or a flat `{ field: scalar }` map";
3
+ export declare const LEAF_OPS: Set<"startsWith" | "eq" | "contains" | "containsExact" | "gte" | "lte" | "lt" | "in" | "isNull" | "isNotNull">;
4
+ export declare function isCompositeFilter(node: unknown): node is CompositeFilter;
5
+ export declare function isLeafFilter(node: unknown): node is LeafFilter;
6
+ /** Flat `{ field: scalar }` map — the 99% listWhere sugar. Empty `{}` is NOT flat (handler throws on listWhere `{}`; other callers keep it opaque). */
7
+ export declare function isFlatEqMap(node: unknown): node is Record<string, string | number | boolean | bigint | Date | null>;
8
+ /**
9
+ * Turns a listWhere / relations.where return value into a Filter when we can
10
+ * do so without guessing. Nested Prisma where objects stay opaque.
11
+ */
12
+ export declare function normalizeScope(scope: Record<string, unknown> | Filter | undefined): Filter | Record<string, unknown> | undefined;
@@ -0,0 +1,72 @@
1
+ export const OPAQUE_FILTER_ERROR = 'nested Prisma `where` is not supported by the Drizzle adapter; return a Filter or a flat `{ field: scalar }` map';
2
+ export const LEAF_OPS = new Set([
3
+ 'eq',
4
+ 'contains',
5
+ 'containsExact',
6
+ 'startsWith',
7
+ 'gte',
8
+ 'lte',
9
+ 'lt',
10
+ 'in',
11
+ 'isNull',
12
+ 'isNotNull'
13
+ ]);
14
+ export function isCompositeFilter(node) {
15
+ return (typeof node === 'object' &&
16
+ node !== null &&
17
+ 'op' in node &&
18
+ (node.op === 'and' || node.op === 'or') &&
19
+ 'clauses' in node &&
20
+ Array.isArray(node.clauses));
21
+ }
22
+ export function isLeafFilter(node) {
23
+ return (typeof node === 'object' &&
24
+ node !== null &&
25
+ 'op' in node &&
26
+ typeof node.op === 'string' &&
27
+ LEAF_OPS.has(node.op) &&
28
+ 'field' in node &&
29
+ typeof node.field === 'string');
30
+ }
31
+ function isScalar(value) {
32
+ return (value === null ||
33
+ typeof value === 'string' ||
34
+ typeof value === 'number' ||
35
+ typeof value === 'boolean' ||
36
+ typeof value === 'bigint' ||
37
+ value instanceof Date);
38
+ }
39
+ /** Flat `{ field: scalar }` map — the 99% listWhere sugar. Empty `{}` is NOT flat (handler throws on listWhere `{}`; other callers keep it opaque). */
40
+ export function isFlatEqMap(node) {
41
+ if (typeof node !== 'object' || node === null || Array.isArray(node))
42
+ return false;
43
+ if (isLeafFilter(node) || isCompositeFilter(node))
44
+ return false;
45
+ const values = Object.values(node);
46
+ if (values.length === 0)
47
+ return false;
48
+ return values.every(isScalar);
49
+ }
50
+ function flatEqMapToFilter(map) {
51
+ const clauses = Object.entries(map).map(([field, value]) => ({
52
+ op: 'eq',
53
+ field,
54
+ value
55
+ }));
56
+ if (clauses.length === 1)
57
+ return clauses[0];
58
+ return { op: 'and', clauses };
59
+ }
60
+ /**
61
+ * Turns a listWhere / relations.where return value into a Filter when we can
62
+ * do so without guessing. Nested Prisma where objects stay opaque.
63
+ */
64
+ export function normalizeScope(scope) {
65
+ if (scope === undefined)
66
+ return undefined;
67
+ if (isLeafFilter(scope) || isCompositeFilter(scope))
68
+ return scope;
69
+ if (isFlatEqMap(scope))
70
+ return flatEqMapToFilter(scope);
71
+ return scope;
72
+ }
@@ -0,0 +1,13 @@
1
+ import type { DataAdapter } from '../types.js';
2
+ /**
3
+ * Prisma implementation of `DataAdapter`. `caseInsensitiveSearch` is fixed
4
+ * at construction time (Task 5's boot block resolves it from the schema's
5
+ * `provider`/`config.search.mode` before building this adapter) — it's the
6
+ * same value `buildWhere`'s caller in `handler.ts` used to pass directly to
7
+ * `buildWhere`, just applied one layer later, at compile-to-Prisma-where
8
+ * time instead of at Filter-construction time. It only affects `contains`
9
+ * leaves; every other leaf/composite op ignores it.
10
+ */
11
+ export declare function createPrismaDataAdapter(prisma: any, opts?: {
12
+ caseInsensitiveSearch?: boolean;
13
+ }): DataAdapter;
@@ -0,0 +1,138 @@
1
+ import { toPrismaModel, primaryKeyOf, coerceId } from '../../data.js';
2
+ import { compileFilterToPrismaWhere } from './filterCompiler.js';
3
+ import { withWriteRetry } from '../retry.js';
4
+ /**
5
+ * Revalidation des cibles de relation à l'intérieur de la transaction d'écriture.
6
+ *
7
+ * Fenêtre résiduelle, assumée : cette lecture ne pose aucun verrou de ligne.
8
+ * Sur PostgreSQL, `Serializable` ne l'empêche PAS — mesuré sur PG 16 : SSI ne
9
+ * voit aucun cycle de dépendances dans « lire le guard -> un tiers sort la
10
+ * cible du scope -> écrire », donc les deux transactions committent. L'adapter
11
+ * Drizzle ferme cette fenêtre avec un `FOR SHARE` ; Prisma n'expose aucune API
12
+ * de verrou, et l'émettre demanderait du `$queryRaw` par dialecte, donc les
13
+ * noms physiques de tables et colonnes (les `@@map`/`@map` ne sont pas parsés)
14
+ * et un second compilateur de filtres à garder en phase avec
15
+ * `compileFilterToPrismaWhere` — exactement le duplicata divergent que ce
16
+ * codebase a déjà payé une fois.
17
+ *
18
+ * L'exposition reste bornée : `mutations.ts` a déjà revalidé chaque FK et m2m
19
+ * par un `findFirst` scopé avant d'appeler l'adapter, et gagner cette course
20
+ * ne produit qu'une référence orpheline inter-tenant — aucune lecture des
21
+ * données de l'autre tenant, le scoping des dropdowns et la « chip » anti-oracle
22
+ * tenant par ailleurs. C'est un défaut d'intégrité, pas une divulgation.
23
+ */
24
+ async function validateTargetGuards(tx, guards, compile) {
25
+ for (const guard of guards) {
26
+ const key = toPrismaModel(guard.targetModel.name);
27
+ const pk = primaryKeyOf(guard.targetModel);
28
+ const where = guard.filter
29
+ ? { [pk]: guard.targetPk, AND: [compile(guard.filter)] }
30
+ : { [pk]: guard.targetPk };
31
+ if (!(await tx[key].findFirst({ where }))) {
32
+ throw new Error('relation target is outside the authorization scope');
33
+ }
34
+ }
35
+ }
36
+ /**
37
+ * Prisma implementation of `DataAdapter`. `caseInsensitiveSearch` is fixed
38
+ * at construction time (Task 5's boot block resolves it from the schema's
39
+ * `provider`/`config.search.mode` before building this adapter) — it's the
40
+ * same value `buildWhere`'s caller in `handler.ts` used to pass directly to
41
+ * `buildWhere`, just applied one layer later, at compile-to-Prisma-where
42
+ * time instead of at Filter-construction time. It only affects `contains`
43
+ * leaves; every other leaf/composite op ignores it.
44
+ */
45
+ export function createPrismaDataAdapter(prisma, opts = {}) {
46
+ const compileHere = (filter) => compileFilterToPrismaWhere(filter, opts.caseInsensitiveSearch ?? false);
47
+ return {
48
+ async listRecords(model, listOpts) {
49
+ const key = toPrismaModel(model.name);
50
+ const primaryKey = primaryKeyOf(model);
51
+ const where = compileHere(listOpts.filter);
52
+ const [rows, total] = await Promise.all([
53
+ prisma[key].findMany({ where, skip: listOpts.skip, take: listOpts.take, orderBy: { [primaryKey]: 'desc' } }),
54
+ prisma[key].count({ where })
55
+ ]);
56
+ return { rows, total };
57
+ },
58
+ findMany(model, findOpts) {
59
+ const key = toPrismaModel(model.name);
60
+ const where = compileHere(findOpts.filter);
61
+ return prisma[key].findMany({ where, orderBy: findOpts.orderBy, skip: findOpts.skip, take: findOpts.take });
62
+ },
63
+ getRecord(model, id) {
64
+ const primaryKey = primaryKeyOf(model);
65
+ return prisma[toPrismaModel(model.name)].findUnique({
66
+ where: { [primaryKey]: coerceId(String(id), model) }
67
+ });
68
+ },
69
+ findFirst(model, filter) {
70
+ const key = toPrismaModel(model.name);
71
+ return prisma[key].findFirst({ where: compileHere(filter) });
72
+ },
73
+ countRecords(model, filter) {
74
+ const key = toPrismaModel(model.name);
75
+ return prisma[key].count({ where: compileHere(filter) });
76
+ },
77
+ async createRecord(model, input) {
78
+ const key = toPrismaModel(model.name);
79
+ const m2mFields = Object.keys(input.m2m ?? {});
80
+ const guards = input.targetGuards ?? [];
81
+ if (m2mFields.length === 0 && guards.length === 0) {
82
+ return prisma[key].create({ data: input.scalars });
83
+ }
84
+ return withWriteRetry(() => prisma.$transaction(async (tx) => {
85
+ await validateTargetGuards(tx, guards, compileHere);
86
+ const data = { ...input.scalars };
87
+ for (const field of m2mFields) {
88
+ const { targetPkField, ids } = input.m2m[field];
89
+ data[field] = { connect: ids.map((id) => ({ [targetPkField]: id })) };
90
+ }
91
+ return tx[key].create({ data });
92
+ }, { isolationLevel: "Serializable" }));
93
+ },
94
+ async updateRecord(model, id, input, authorizationFilter) {
95
+ const key = toPrismaModel(model.name);
96
+ const primaryKey = primaryKeyOf(model);
97
+ const where = authorizationFilter
98
+ ? { [primaryKey]: coerceId(String(id), model), AND: [compileHere(authorizationFilter)] }
99
+ : { [primaryKey]: coerceId(String(id), model) };
100
+ const m2mFields = Object.keys(input.m2m ?? {});
101
+ const guards = input.targetGuards ?? [];
102
+ if (m2mFields.length === 0 && guards.length === 0) {
103
+ return prisma[key].update({ where, data: input.scalars });
104
+ }
105
+ return withWriteRetry(() => prisma.$transaction(async (tx) => {
106
+ await validateTargetGuards(tx, guards, compileHere);
107
+ const data = { ...input.scalars };
108
+ for (const field of m2mFields) {
109
+ const { targetPkField, ids } = input.m2m[field];
110
+ data[field] = { set: ids.map((id) => ({ [targetPkField]: id })) };
111
+ }
112
+ return tx[key].update({ where, data });
113
+ }, { isolationLevel: "Serializable" }));
114
+ },
115
+ async deleteRecord(model, id, authorizationFilter) {
116
+ const primaryKey = primaryKeyOf(model);
117
+ const where = authorizationFilter
118
+ ? { [primaryKey]: coerceId(String(id), model), AND: [compileHere(authorizationFilter)] }
119
+ : { [primaryKey]: coerceId(String(id), model) };
120
+ await prisma[toPrismaModel(model.name)].delete({ where });
121
+ },
122
+ async getM2mSelectedIds(model, edge, targetModel, recordId) {
123
+ try {
124
+ const primaryKey = primaryKeyOf(model);
125
+ const current = await prisma[toPrismaModel(model.name)].findUnique({
126
+ where: { [primaryKey]: coerceId(String(recordId), model) },
127
+ include: { [edge.field]: true }
128
+ });
129
+ const linked = current?.[edge.field] ?? [];
130
+ const targetPk = primaryKeyOf(targetModel);
131
+ return linked.map((row) => row[targetPk]);
132
+ }
133
+ catch {
134
+ return [];
135
+ }
136
+ }
137
+ };
138
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Compiles the generic `Filter` AST (see ../types.ts) into a Prisma `where`
3
+ * object. This is the ONLY place in the Prisma adapter that knows Prisma's
4
+ * where-clause vocabulary (`AND`/`OR`/`contains`/`startsWith`/`gte`/`lte`/
5
+ * `equals`/`not`/`in`/`mode: 'insensitive'`).
6
+ */
7
+ import type { Filter } from '../types.js';
8
+ export type PrismaWhere = Record<string, unknown>;
9
+ export declare function compileFilterToPrismaWhere(filter: Filter | PrismaWhere | undefined, caseInsensitiveSearch: boolean): PrismaWhere | undefined;
@@ -0,0 +1,42 @@
1
+ import { isCompositeFilter, isLeafFilter } from '../filter.js';
2
+ function compileLeaf(filter, caseInsensitiveSearch) {
3
+ switch (filter.op) {
4
+ case 'eq':
5
+ return { [filter.field]: filter.value };
6
+ case 'contains':
7
+ return caseInsensitiveSearch
8
+ ? { [filter.field]: { contains: filter.value, mode: 'insensitive' } }
9
+ : { [filter.field]: { contains: filter.value } };
10
+ case 'containsExact':
11
+ return { [filter.field]: { contains: filter.value } };
12
+ case 'startsWith':
13
+ return { [filter.field]: { startsWith: filter.value } };
14
+ case 'gte':
15
+ return { [filter.field]: { gte: filter.value } };
16
+ case 'lte':
17
+ return { [filter.field]: { lte: filter.value } };
18
+ case 'lt':
19
+ return { [filter.field]: { lt: filter.value } };
20
+ case 'in':
21
+ return { [filter.field]: { in: filter.value } };
22
+ case 'isNull':
23
+ return { [filter.field]: { equals: null } };
24
+ case 'isNotNull':
25
+ return { [filter.field]: { not: null } };
26
+ }
27
+ }
28
+ function compile(node, caseInsensitiveSearch) {
29
+ if (isCompositeFilter(node)) {
30
+ const clauses = node.clauses.map((c) => compile(c, caseInsensitiveSearch));
31
+ return node.op === 'and' ? { AND: clauses } : { OR: clauses };
32
+ }
33
+ if (isLeafFilter(node)) {
34
+ return compileLeaf(node, caseInsensitiveSearch);
35
+ }
36
+ return node;
37
+ }
38
+ export function compileFilterToPrismaWhere(filter, caseInsensitiveSearch) {
39
+ if (filter === undefined)
40
+ return undefined;
41
+ return compile(filter, caseInsensitiveSearch);
42
+ }
@@ -0,0 +1,14 @@
1
+ import { type AdminHandlerConfig as CoreAdminHandlerConfig } from '../../handler.js';
2
+ export interface AdminHandlerConfig extends Omit<CoreAdminHandlerConfig, 'adapter' | 'prisma' | 'prismaSchemaPath' | 'search'> {
3
+ prisma?: any;
4
+ prismaSchemaPath?: string;
5
+ adapter?: CoreAdminHandlerConfig['adapter'];
6
+ search?: {
7
+ mode?: 'auto' | 'insensitive' | 'default';
8
+ };
9
+ }
10
+ export declare function createAdminHandler(config: AdminHandlerConfig): ({ event, resolve }: {
11
+ event: any;
12
+ resolve: (event: any) => Response | Promise<Response>;
13
+ }) => Promise<Response>;
14
+ export type { AdminPlugin, AdminPluginPage, AdminPluginRecordAction, PluginPageContext, PluginPageResult } from '../../plugin.js';
@@ -0,0 +1,37 @@
1
+ import { createAdminHandler as createCoreHandler } from '../../handler.js';
2
+ import { createPrismaDataAdapter } from './dataAdapter.js';
3
+ import { createPrismaIntrospector } from './introspector.js';
4
+ import { resolveCaseInsensitiveSearch } from './index.js';
5
+ function omitPrismaShortcutFields(config) {
6
+ const { prisma: _prisma, prismaSchemaPath: _path, search: _search, adapter: _adapter, ...rest } = config;
7
+ return rest;
8
+ }
9
+ function buildPrismaAdapter(config) {
10
+ const schemaPath = config.prismaSchemaPath ?? './prisma/schema.prisma';
11
+ const introspector = createPrismaIntrospector({ schemaPath });
12
+ let schema = null;
13
+ try {
14
+ schema = introspector.introspect();
15
+ }
16
+ catch {
17
+ schema = null;
18
+ }
19
+ return {
20
+ introspector: schema ? { introspect: () => schema } : introspector,
21
+ data: createPrismaDataAdapter(config.prisma, {
22
+ caseInsensitiveSearch: resolveCaseInsensitiveSearch(schema, config.search?.mode)
23
+ })
24
+ };
25
+ }
26
+ export function createAdminHandler(config) {
27
+ if (config.adapter) {
28
+ return createCoreHandler({ ...omitPrismaShortcutFields(config), adapter: config.adapter });
29
+ }
30
+ if (!config.prisma) {
31
+ throw new Error('[sveltekit-admin] createAdminHandler requires either `prisma` (with optional `prismaSchemaPath`) or `adapter` — neither was provided.');
32
+ }
33
+ return createCoreHandler({
34
+ ...omitPrismaShortcutFields(config),
35
+ adapter: buildPrismaAdapter(config)
36
+ });
37
+ }
@@ -0,0 +1,28 @@
1
+ import type { SchemaIntrospector, DataAdapter } from '../types.js';
2
+ import type { Schema } from '../../types/schema.js';
3
+ export { compileFilterToPrismaWhere } from './filterCompiler.js';
4
+ /**
5
+ * `mode: 'insensitive'` is only valid Prisma-side on postgresql/cockroachdb/
6
+ * mongodb — emitting it on sqlite/mysql/sqlserver is a hard Prisma error.
7
+ * `schema` is `null` when introspection failed (the caller degraded
8
+ * gracefully instead of throwing) — in that case `'auto'` can never detect a
9
+ * supported provider, so it resolves to `false`, same as today.
10
+ */
11
+ export declare function resolveCaseInsensitiveSearch(schema: Schema | null, searchMode?: 'auto' | 'insensitive' | 'default'): boolean;
12
+ /**
13
+ * Builds a ready-to-use Prisma adapter. Introspects eagerly (throws
14
+ * immediately on a bad `schemaPath`) — this is the explicit-construction
15
+ * path (`createAdminHandler({ adapter: createPrismaAdapter(...) })`), where
16
+ * failing loud on a caller's own mistake is correct. `createAdminHandler`'s
17
+ * OWN legacy `{ prisma, prismaSchemaPath }` path does NOT call this function
18
+ * — it builds the introspector/data pair itself so it can keep degrading
19
+ * gracefully to "no models known" on a bad path, exactly as it always has.
20
+ */
21
+ export declare function createPrismaAdapter(opts: {
22
+ prisma: any;
23
+ schemaPath: string;
24
+ searchMode?: 'auto' | 'insensitive' | 'default';
25
+ }): {
26
+ introspector: SchemaIntrospector;
27
+ data: DataAdapter;
28
+ };
@@ -0,0 +1,32 @@
1
+ import { createPrismaIntrospector } from './introspector.js';
2
+ import { createPrismaDataAdapter } from './dataAdapter.js';
3
+ export { compileFilterToPrismaWhere } from './filterCompiler.js';
4
+ /**
5
+ * `mode: 'insensitive'` is only valid Prisma-side on postgresql/cockroachdb/
6
+ * mongodb — emitting it on sqlite/mysql/sqlserver is a hard Prisma error.
7
+ * `schema` is `null` when introspection failed (the caller degraded
8
+ * gracefully instead of throwing) — in that case `'auto'` can never detect a
9
+ * supported provider, so it resolves to `false`, same as today.
10
+ */
11
+ export function resolveCaseInsensitiveSearch(schema, searchMode = 'auto') {
12
+ return (searchMode === 'insensitive' ||
13
+ (searchMode === 'auto' && ['postgresql', 'cockroachdb', 'mongodb'].includes(schema?.provider ?? '')));
14
+ }
15
+ /**
16
+ * Builds a ready-to-use Prisma adapter. Introspects eagerly (throws
17
+ * immediately on a bad `schemaPath`) — this is the explicit-construction
18
+ * path (`createAdminHandler({ adapter: createPrismaAdapter(...) })`), where
19
+ * failing loud on a caller's own mistake is correct. `createAdminHandler`'s
20
+ * OWN legacy `{ prisma, prismaSchemaPath }` path does NOT call this function
21
+ * — it builds the introspector/data pair itself so it can keep degrading
22
+ * gracefully to "no models known" on a bad path, exactly as it always has.
23
+ */
24
+ export function createPrismaAdapter(opts) {
25
+ const introspector = createPrismaIntrospector({ schemaPath: opts.schemaPath });
26
+ const schema = introspector.introspect();
27
+ const caseInsensitiveSearch = resolveCaseInsensitiveSearch(schema, opts.searchMode);
28
+ return {
29
+ introspector: { introspect: () => schema }, // memoized: introspect() already ran once above
30
+ data: createPrismaDataAdapter(opts.prisma, { caseInsensitiveSearch })
31
+ };
32
+ }
@@ -0,0 +1,4 @@
1
+ import type { SchemaIntrospector } from '../types.js';
2
+ export declare function createPrismaIntrospector(opts: {
3
+ schemaPath: string;
4
+ }): SchemaIntrospector;
@@ -0,0 +1,9 @@
1
+ /** Wraps the existing regex-based `.prisma` file parser behind `SchemaIntrospector`. */
2
+ import { parsePrismaSchema } from '../../introspection/parser.js';
3
+ export function createPrismaIntrospector(opts) {
4
+ return {
5
+ introspect() {
6
+ return parsePrismaSchema(opts.schemaPath);
7
+ }
8
+ };
9
+ }