sveltekit-admin 0.5.3 → 0.6.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 +32 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/server/adapters/drizzle/dataAdapter.d.ts +11 -0
- package/dist/server/adapters/drizzle/dataAdapter.js +243 -0
- package/dist/server/adapters/drizzle/filterCompiler.d.ts +8 -0
- package/dist/server/adapters/drizzle/filterCompiler.js +61 -0
- package/dist/server/adapters/drizzle/index.d.ts +13 -0
- package/dist/server/adapters/drizzle/index.js +23 -0
- package/dist/server/adapters/drizzle/inspect.d.ts +23 -0
- package/dist/server/adapters/drizzle/inspect.js +220 -0
- package/dist/server/adapters/drizzle/introspector.d.ts +3 -0
- package/dist/server/adapters/drizzle/introspector.js +3 -0
- package/dist/server/adapters/filter.d.ts +12 -0
- package/dist/server/adapters/filter.js +72 -0
- package/dist/server/adapters/prisma/dataAdapter.d.ts +13 -0
- package/dist/server/adapters/prisma/dataAdapter.js +96 -0
- package/dist/server/adapters/prisma/filterCompiler.d.ts +9 -0
- package/dist/server/adapters/prisma/filterCompiler.js +42 -0
- package/dist/server/adapters/prisma/index.d.ts +28 -0
- package/dist/server/adapters/prisma/index.js +32 -0
- package/dist/server/adapters/prisma/introspector.d.ts +4 -0
- package/dist/server/adapters/prisma/introspector.js +9 -0
- package/dist/server/adapters/types.d.ts +79 -0
- package/dist/server/adapters/types.js +1 -0
- package/dist/server/data.d.ts +0 -8
- package/dist/server/data.js +0 -41
- package/dist/server/handler.d.ts +18 -2
- package/dist/server/handler.js +110 -84
- package/dist/server/introspection/parser.d.ts +4 -40
- package/dist/server/introspection/relations.d.ts +1 -1
- package/dist/server/introspection/relations.js +1 -1
- package/dist/server/query/listQuery.d.ts +6 -12
- package/dist/server/query/listQuery.js +31 -53
- package/dist/server/types/schema.d.ts +40 -0
- package/dist/server/types/schema.js +8 -0
- package/dist/server/views/Form.svelte +1 -1
- package/dist/server/views/RelationCheckboxes.svelte +1 -1
- package/package.json +23 -2
|
@@ -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,96 @@
|
|
|
1
|
+
import { toPrismaModel, primaryKeyOf, coerceId } from '../../data.js';
|
|
2
|
+
import { compileFilterToPrismaWhere } from './filterCompiler.js';
|
|
3
|
+
/**
|
|
4
|
+
* Prisma implementation of `DataAdapter`. `caseInsensitiveSearch` is fixed
|
|
5
|
+
* at construction time (Task 5's boot block resolves it from the schema's
|
|
6
|
+
* `provider`/`config.search.mode` before building this adapter) — it's the
|
|
7
|
+
* same value `buildWhere`'s caller in `handler.ts` used to pass directly to
|
|
8
|
+
* `buildWhere`, just applied one layer later, at compile-to-Prisma-where
|
|
9
|
+
* time instead of at Filter-construction time. It only affects `contains`
|
|
10
|
+
* leaves; every other leaf/composite op ignores it.
|
|
11
|
+
*/
|
|
12
|
+
export function createPrismaDataAdapter(prisma, opts = {}) {
|
|
13
|
+
const compileHere = (filter) => compileFilterToPrismaWhere(filter, opts.caseInsensitiveSearch ?? false);
|
|
14
|
+
return {
|
|
15
|
+
async listRecords(model, listOpts) {
|
|
16
|
+
const key = toPrismaModel(model.name);
|
|
17
|
+
const primaryKey = primaryKeyOf(model);
|
|
18
|
+
const where = compileHere(listOpts.filter);
|
|
19
|
+
const [rows, total] = await Promise.all([
|
|
20
|
+
prisma[key].findMany({ where, skip: listOpts.skip, take: listOpts.take, orderBy: { [primaryKey]: 'desc' } }),
|
|
21
|
+
prisma[key].count({ where })
|
|
22
|
+
]);
|
|
23
|
+
return { rows, total };
|
|
24
|
+
},
|
|
25
|
+
findMany(model, findOpts) {
|
|
26
|
+
const key = toPrismaModel(model.name);
|
|
27
|
+
const where = compileHere(findOpts.filter);
|
|
28
|
+
return prisma[key].findMany({ where, orderBy: findOpts.orderBy, skip: findOpts.skip, take: findOpts.take });
|
|
29
|
+
},
|
|
30
|
+
getRecord(model, id) {
|
|
31
|
+
const primaryKey = primaryKeyOf(model);
|
|
32
|
+
return prisma[toPrismaModel(model.name)].findUnique({
|
|
33
|
+
where: { [primaryKey]: coerceId(String(id), model) }
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
findFirst(model, filter) {
|
|
37
|
+
const key = toPrismaModel(model.name);
|
|
38
|
+
return prisma[key].findFirst({ where: compileHere(filter) });
|
|
39
|
+
},
|
|
40
|
+
countRecords(model, filter) {
|
|
41
|
+
const key = toPrismaModel(model.name);
|
|
42
|
+
return prisma[key].count({ where: compileHere(filter) });
|
|
43
|
+
},
|
|
44
|
+
async createRecord(model, input) {
|
|
45
|
+
const key = toPrismaModel(model.name);
|
|
46
|
+
const m2mFields = Object.keys(input.m2m ?? {});
|
|
47
|
+
if (m2mFields.length === 0) {
|
|
48
|
+
return prisma[key].create({ data: input.scalars });
|
|
49
|
+
}
|
|
50
|
+
return prisma.$transaction(async (tx) => {
|
|
51
|
+
const data = { ...input.scalars };
|
|
52
|
+
for (const field of m2mFields) {
|
|
53
|
+
const { targetPkField, ids } = input.m2m[field];
|
|
54
|
+
data[field] = { connect: ids.map((id) => ({ [targetPkField]: id })) };
|
|
55
|
+
}
|
|
56
|
+
return tx[key].create({ data });
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
async updateRecord(model, id, input) {
|
|
60
|
+
const key = toPrismaModel(model.name);
|
|
61
|
+
const primaryKey = primaryKeyOf(model);
|
|
62
|
+
const where = { [primaryKey]: coerceId(String(id), model) };
|
|
63
|
+
const m2mFields = Object.keys(input.m2m ?? {});
|
|
64
|
+
if (m2mFields.length === 0) {
|
|
65
|
+
return prisma[key].update({ where, data: input.scalars });
|
|
66
|
+
}
|
|
67
|
+
return prisma.$transaction(async (tx) => {
|
|
68
|
+
const data = { ...input.scalars };
|
|
69
|
+
for (const field of m2mFields) {
|
|
70
|
+
const { targetPkField, ids } = input.m2m[field];
|
|
71
|
+
data[field] = { set: ids.map((id) => ({ [targetPkField]: id })) };
|
|
72
|
+
}
|
|
73
|
+
return tx[key].update({ where, data });
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
async deleteRecord(model, id) {
|
|
77
|
+
const primaryKey = primaryKeyOf(model);
|
|
78
|
+
await prisma[toPrismaModel(model.name)].delete({ where: { [primaryKey]: coerceId(String(id), model) } });
|
|
79
|
+
},
|
|
80
|
+
async getM2mSelectedIds(model, edge, targetModel, recordId) {
|
|
81
|
+
try {
|
|
82
|
+
const primaryKey = primaryKeyOf(model);
|
|
83
|
+
const current = await prisma[toPrismaModel(model.name)].findUnique({
|
|
84
|
+
where: { [primaryKey]: coerceId(String(recordId), model) },
|
|
85
|
+
include: { [edge.field]: true }
|
|
86
|
+
});
|
|
87
|
+
const linked = current?.[edge.field] ?? [];
|
|
88
|
+
const targetPk = primaryKeyOf(targetModel);
|
|
89
|
+
return linked.map((row) => row[targetPk]);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -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,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,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
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORM-agnostic where-clause AST. `listQuery.ts#buildWhere` produces this;
|
|
3
|
+
* each adapter's own `filterCompiler` (see `adapters/prisma/filterCompiler.ts`)
|
|
4
|
+
* turns it into that ORM's native query shape. Never expose an ORM-specific
|
|
5
|
+
* operator here (no `mode: 'insensitive'`, no Prisma `not`) — those are
|
|
6
|
+
* compiler-side decisions made from `LeafFilter.op`, not carried in the AST.
|
|
7
|
+
*/
|
|
8
|
+
export type Filter = CompositeFilter | LeafFilter;
|
|
9
|
+
export interface CompositeFilter {
|
|
10
|
+
op: 'and' | 'or';
|
|
11
|
+
clauses: Filter[];
|
|
12
|
+
}
|
|
13
|
+
export interface LeafFilter {
|
|
14
|
+
op: 'eq' | 'contains' | 'containsExact' | 'startsWith' | 'gte' | 'lte' | 'lt' | 'in' | 'isNull' | 'isNotNull';
|
|
15
|
+
field: string;
|
|
16
|
+
value?: unknown;
|
|
17
|
+
}
|
|
18
|
+
import type { Schema, Model } from '../types/schema.js';
|
|
19
|
+
import type { RelationEdge } from '../introspection/relations.js';
|
|
20
|
+
/** Boot-time schema source. One call per handler lifetime — no per-request cost. */
|
|
21
|
+
export interface SchemaIntrospector {
|
|
22
|
+
introspect(): Schema | Promise<Schema>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Per-request CRUD + relation-read surface `handler.ts` talks to instead of
|
|
26
|
+
* a raw ORM client. See docs/superpowers/specs/2026-08-13-db-adapter-abstraction-design.md
|
|
27
|
+
* for the rationale behind each method's shape.
|
|
28
|
+
*/
|
|
29
|
+
export interface DataAdapter {
|
|
30
|
+
/** Vue liste paginée : toujours tri PK desc, toujours count + fetch ensemble. */
|
|
31
|
+
listRecords(model: Model, opts: {
|
|
32
|
+
filter?: Filter;
|
|
33
|
+
skip: number;
|
|
34
|
+
take: number;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
rows: Record<string, unknown>[];
|
|
37
|
+
total: number;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Lecture générale sans pagination forcée : options de relation FK/m2m,
|
|
41
|
+
* options de filtre FK sidebar, endpoint `_search`. `orderBy` est le
|
|
42
|
+
* `Record<string, 'asc' | 'desc'>` déjà exposé tel quel côté config
|
|
43
|
+
* publique (`AdminHandlerConfig.models[].relations[field].orderBy`) —
|
|
44
|
+
* transmis de façon opaque, sans traduction.
|
|
45
|
+
*/
|
|
46
|
+
findMany(model: Model, opts: {
|
|
47
|
+
filter?: Filter;
|
|
48
|
+
orderBy?: Record<string, 'asc' | 'desc'>;
|
|
49
|
+
skip?: number;
|
|
50
|
+
take?: number;
|
|
51
|
+
}): Promise<Record<string, unknown>[]>;
|
|
52
|
+
getRecord(model: Model, id: string | number): Promise<Record<string, unknown> | null>;
|
|
53
|
+
findFirst(model: Model, filter: Filter): Promise<Record<string, unknown> | null>;
|
|
54
|
+
countRecords(model: Model, filter?: Filter): Promise<number>;
|
|
55
|
+
/**
|
|
56
|
+
* `m2m`'s value carries the TARGET model's PK field name alongside the raw
|
|
57
|
+
* ids, not just the ids: this adapter has no `Schema`/`RelationGraph` of
|
|
58
|
+
* its own to resolve a target model from an edge, and `handler.ts` (the
|
|
59
|
+
* only caller) already resolves the target model before building this
|
|
60
|
+
* payload, at zero extra cost to it.
|
|
61
|
+
*/
|
|
62
|
+
createRecord(model: Model, input: {
|
|
63
|
+
scalars: Record<string, unknown>;
|
|
64
|
+
m2m?: Record<string, {
|
|
65
|
+
targetPkField: string;
|
|
66
|
+
ids: Array<string | number>;
|
|
67
|
+
}>;
|
|
68
|
+
}): Promise<Record<string, unknown>>;
|
|
69
|
+
updateRecord(model: Model, id: string | number, input: {
|
|
70
|
+
scalars: Record<string, unknown>;
|
|
71
|
+
m2m?: Record<string, {
|
|
72
|
+
targetPkField: string;
|
|
73
|
+
ids: Array<string | number>;
|
|
74
|
+
}>;
|
|
75
|
+
}): Promise<Record<string, unknown>>;
|
|
76
|
+
deleteRecord(model: Model, id: string | number): Promise<void>;
|
|
77
|
+
/** `targetModel` est fourni par l'appelant : chaque site d'appel actuel l'a déjà résolu. */
|
|
78
|
+
getM2mSelectedIds(model: Model, edge: RelationEdge, targetModel: Model, recordId: string | number): Promise<Array<string | number>>;
|
|
79
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/server/data.d.ts
CHANGED
|
@@ -28,11 +28,3 @@ export declare function paginate(pageParam: string | null, perPage: number): {
|
|
|
28
28
|
skip: number;
|
|
29
29
|
take: number;
|
|
30
30
|
};
|
|
31
|
-
export declare function listRecords(prisma: any, model: PrismaModel, page: number, perPage: number, where?: Record<string, unknown>): Promise<{
|
|
32
|
-
items: any[];
|
|
33
|
-
total: number;
|
|
34
|
-
}>;
|
|
35
|
-
export declare function getRecord(prisma: any, model: PrismaModel, id: string): Promise<any | null>;
|
|
36
|
-
export declare function createRecord(prisma: any, model: PrismaModel, data: Record<string, unknown>): Promise<void>;
|
|
37
|
-
export declare function updateRecord(prisma: any, model: PrismaModel, id: string, data: Record<string, unknown>): Promise<void>;
|
|
38
|
-
export declare function deleteRecord(prisma: any, model: PrismaModel, id: string): Promise<void>;
|
package/dist/server/data.js
CHANGED
|
@@ -73,44 +73,3 @@ export function paginate(pageParam, perPage) {
|
|
|
73
73
|
const page = Number.isSafeInteger(parsed) && parsed >= 1 ? parsed : 1;
|
|
74
74
|
return { page, skip: (page - 1) * perPage, take: perPage };
|
|
75
75
|
}
|
|
76
|
-
export async function listRecords(prisma, model, page, perPage, where) {
|
|
77
|
-
const key = toPrismaModel(model.name);
|
|
78
|
-
const primaryKey = primaryKeyOf(model);
|
|
79
|
-
// Le calcul `(page - 1) * perPage` est dupliqué avec `paginate` DÉLIBÉRÉMENT.
|
|
80
|
-
// Router `listRecords` vers `paginate(String(page), perPage)` serait lossy : la
|
|
81
|
-
// conversion aller-retour passe par `parseInt`, et `String(1e24)` donne
|
|
82
|
-
// `'1e+24'`, que `parseInt` ramène à `1`. `listRecords` reçoit un `number` déjà
|
|
83
|
-
// validé par l'appelant ; ne pas « simplifier » en réintroduisant un parse.
|
|
84
|
-
const [items, total] = await Promise.all([
|
|
85
|
-
prisma[key].findMany({
|
|
86
|
-
where,
|
|
87
|
-
skip: (page - 1) * perPage,
|
|
88
|
-
take: perPage,
|
|
89
|
-
orderBy: { [primaryKey]: 'desc' }
|
|
90
|
-
}),
|
|
91
|
-
prisma[key].count({ where })
|
|
92
|
-
]);
|
|
93
|
-
return { items, total };
|
|
94
|
-
}
|
|
95
|
-
export function getRecord(prisma, model, id) {
|
|
96
|
-
const primaryKey = primaryKeyOf(model);
|
|
97
|
-
return prisma[toPrismaModel(model.name)].findUnique({
|
|
98
|
-
where: { [primaryKey]: coerceId(id, model) }
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
export async function createRecord(prisma, model, data) {
|
|
102
|
-
await prisma[toPrismaModel(model.name)].create({ data });
|
|
103
|
-
}
|
|
104
|
-
export async function updateRecord(prisma, model, id, data) {
|
|
105
|
-
const primaryKey = primaryKeyOf(model);
|
|
106
|
-
await prisma[toPrismaModel(model.name)].update({
|
|
107
|
-
where: { [primaryKey]: coerceId(id, model) },
|
|
108
|
-
data
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
export async function deleteRecord(prisma, model, id) {
|
|
112
|
-
const primaryKey = primaryKeyOf(model);
|
|
113
|
-
await prisma[toPrismaModel(model.name)].delete({
|
|
114
|
-
where: { [primaryKey]: coerceId(id, model) }
|
|
115
|
-
});
|
|
116
|
-
}
|
package/dist/server/handler.d.ts
CHANGED
|
@@ -2,11 +2,27 @@
|
|
|
2
2
|
* SvelteKit Admin - Standalone Handler
|
|
3
3
|
* Zero files needed in routes - everything handled via hook
|
|
4
4
|
*/
|
|
5
|
+
import type { DataAdapter, SchemaIntrospector } from './adapters/types.js';
|
|
5
6
|
export interface AdminHandlerConfig {
|
|
6
|
-
/**
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Prisma client instance. Required unless `adapter` is provided directly —
|
|
9
|
+
* exactly one of the two must be set. Kept required-looking here (not `?`)
|
|
10
|
+
* for source compatibility with every existing call site; passing neither
|
|
11
|
+
* throws at handler-creation time (see the boot block).
|
|
12
|
+
*/
|
|
13
|
+
prisma?: any;
|
|
8
14
|
/** Path to Prisma schema file */
|
|
9
15
|
prismaSchemaPath?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Explicit adapter, built via `createPrismaAdapter(...)` (or, in a future
|
|
18
|
+
* release, a Drizzle/other adapter). Takes priority over `prisma`/
|
|
19
|
+
* `prismaSchemaPath` when both are somehow set. Most consumers never touch
|
|
20
|
+
* this — passing `prisma`/`prismaSchemaPath` builds one internally.
|
|
21
|
+
*/
|
|
22
|
+
adapter?: {
|
|
23
|
+
introspector: SchemaIntrospector;
|
|
24
|
+
data: DataAdapter;
|
|
25
|
+
};
|
|
10
26
|
/** Base path for admin routes (default: /admin) */
|
|
11
27
|
basePath?: string;
|
|
12
28
|
/** Authentication check - return true if user can access admin */
|