@edium/halifax 2.1.0 → 2.2.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/README.md +102 -17
  3. package/README_AUTH.md +38 -0
  4. package/README_AUTOCRUD.md +5 -5
  5. package/README_CLASSES.md +322 -0
  6. package/README_HOOKS.md +275 -0
  7. package/README_INTERFACES.md +601 -0
  8. package/README_OPENAPI.md +471 -0
  9. package/README_REPO_ADAPTERS.md +77 -0
  10. package/README_TYPES.md +114 -0
  11. package/dist/adapters/orm/drizzle/DrizzleAdapter.d.ts +128 -0
  12. package/dist/adapters/orm/drizzle/DrizzleAdapter.js +255 -0
  13. package/dist/adapters/orm/drizzle/astToDrizzle.d.ts +21 -0
  14. package/dist/adapters/orm/drizzle/astToDrizzle.js +121 -0
  15. package/dist/adapters/orm/drizzle/index.d.ts +4 -0
  16. package/dist/adapters/orm/drizzle/index.js +2 -0
  17. package/dist/adapters/orm/prisma/PrismaAdapter.d.ts +1 -1
  18. package/dist/adapters/orm/prisma/PrismaAdapter.js +24 -1
  19. package/dist/adapters/orm/prisma/astToPrisma.d.ts +1 -2
  20. package/dist/adapters/orm/prisma/astToPrisma.js +1 -3
  21. package/dist/adapters/orm/prisma/helpers.js +1 -1
  22. package/dist/adapters/orm/prisma/types.d.ts +11 -11
  23. package/dist/auth/AuthStrategy.d.ts +6 -189
  24. package/dist/auth/AuthStrategy.js +4 -220
  25. package/dist/auth/strategies/AllowAllAuthStrategy.d.ts +6 -0
  26. package/dist/auth/strategies/AllowAllAuthStrategy.js +6 -0
  27. package/dist/auth/strategies/ApiKeyAuthStrategy.d.ts +25 -0
  28. package/dist/auth/strategies/ApiKeyAuthStrategy.js +39 -0
  29. package/dist/auth/strategies/JwtClaimsAuthStrategy.d.ts +32 -0
  30. package/dist/auth/strategies/JwtClaimsAuthStrategy.js +52 -0
  31. package/dist/auth/strategies/PassportStrategies.d.ts +94 -0
  32. package/dist/auth/strategies/PassportStrategies.js +142 -0
  33. package/dist/auth/strategies/types.d.ts +70 -0
  34. package/dist/core/crudRouter.d.ts +11 -18
  35. package/dist/core/crudRouter.js +95 -390
  36. package/dist/core/fields.d.ts +8 -0
  37. package/dist/core/fields.js +14 -0
  38. package/dist/core/handlerUtils.d.ts +70 -0
  39. package/dist/core/handlerUtils.js +193 -0
  40. package/dist/core/handlers/create.d.ts +3 -0
  41. package/dist/core/handlers/create.js +26 -0
  42. package/dist/core/handlers/deleteMany.d.ts +3 -0
  43. package/dist/core/handlers/deleteMany.js +24 -0
  44. package/dist/core/handlers/deleteOne.d.ts +3 -0
  45. package/dist/core/handlers/deleteOne.js +19 -0
  46. package/dist/core/handlers/query.d.ts +3 -0
  47. package/dist/core/handlers/query.js +23 -0
  48. package/dist/core/handlers/readMany.d.ts +3 -0
  49. package/dist/core/handlers/readMany.js +18 -0
  50. package/dist/core/handlers/readOne.d.ts +3 -0
  51. package/dist/core/handlers/readOne.js +23 -0
  52. package/dist/core/handlers/updateMany.d.ts +3 -0
  53. package/dist/core/handlers/updateMany.js +34 -0
  54. package/dist/core/handlers/updateOne.d.ts +3 -0
  55. package/dist/core/handlers/updateOne.js +20 -0
  56. package/dist/core/handlers/upsertOne.d.ts +3 -0
  57. package/dist/core/handlers/upsertOne.js +20 -0
  58. package/dist/core/hooks.d.ts +217 -0
  59. package/dist/core/queryString.js +1 -1
  60. package/dist/core/types.d.ts +38 -29
  61. package/dist/core/validation.d.ts +1 -2
  62. package/dist/core/validation.js +1 -3
  63. package/dist/index.d.ts +3 -6
  64. package/dist/index.js +3 -6
  65. package/dist/openapi/generateDocsHtml.d.ts +1 -0
  66. package/dist/openapi/generateDocsHtml.js +47 -0
  67. package/dist/openapi/index.d.ts +3 -0
  68. package/dist/openapi/index.js +2 -0
  69. package/dist/openapi/specGenerator.d.ts +149 -0
  70. package/dist/openapi/specGenerator.js +770 -0
  71. package/package.json +38 -22
  72. package/dist/enums/SqlComparison.d.ts +0 -28
  73. package/dist/enums/SqlComparison.js +0 -29
  74. package/dist/enums/SqlOperator.d.ts +0 -5
  75. package/dist/enums/SqlOperator.js +0 -6
  76. package/dist/enums/SqlOrder.d.ts +0 -5
  77. package/dist/enums/SqlOrder.js +0 -6
  78. package/dist/interfaces/IQueryFilter.d.ts +0 -17
  79. package/dist/interfaces/IQueryOptions.d.ts +0 -20
  80. package/dist/interfaces/ISort.d.ts +0 -8
  81. package/dist/interfaces/ISort.js +0 -1
  82. /package/dist/{interfaces/IQueryFilter.js → auth/strategies/types.js} +0 -0
  83. /package/dist/{interfaces/IQueryOptions.js → core/hooks.js} +0 -0
@@ -0,0 +1,128 @@
1
+ import type { AnyColumn, SQL, Table } from 'drizzle-orm';
2
+ import type { IQueryOptions } from '@edium/halifax-types';
3
+ import type { Repository, FieldDefinition, ListOptions, ListResult, QueryResult, UpdateManyResult, DeleteManyResult, TenantScope } from '../../../core/types.js';
4
+ type DrizzleOrderByArg = SQL | AnyColumn | ((aliases: Record<string, AnyColumn>) => unknown);
5
+ /**
6
+ * A dynamic Drizzle SELECT chain (returned after calling `.$dynamic()`).
7
+ * All methods remain on the type regardless of call order.
8
+ */
9
+ interface DrizzleDynamicSelect extends PromiseLike<unknown[]> {
10
+ where(cond?: SQL): DrizzleDynamicSelect;
11
+ orderBy(...cols: DrizzleOrderByArg[]): DrizzleDynamicSelect;
12
+ limit(n: number): DrizzleDynamicSelect;
13
+ offset(n: number): DrizzleDynamicSelect;
14
+ }
15
+ /** The builder returned from `.from()` before dynamic mode is activated. */
16
+ interface DrizzleSelectBuilder extends PromiseLike<unknown[]> {
17
+ $dynamic(): DrizzleDynamicSelect;
18
+ where(cond?: SQL): PromiseLike<unknown[]>;
19
+ }
20
+ interface DrizzleFromChain {
21
+ from(table: Table): DrizzleSelectBuilder;
22
+ }
23
+ interface DrizzleUpdateWhereChain {
24
+ returning(): Promise<unknown[]>;
25
+ }
26
+ interface DrizzleUpdateSetChain {
27
+ where(cond?: SQL): DrizzleUpdateWhereChain;
28
+ }
29
+ interface DrizzleUpdateChain {
30
+ set(data: Record<string, unknown>): DrizzleUpdateSetChain;
31
+ }
32
+ interface DrizzleDeleteWhereChain {
33
+ returning(): Promise<unknown[]>;
34
+ }
35
+ interface DrizzleDeleteChain {
36
+ where(cond?: SQL): DrizzleDeleteWhereChain;
37
+ }
38
+ interface DrizzleInsertValuesChain {
39
+ returning(): Promise<unknown[]>;
40
+ }
41
+ interface DrizzleInsertChain {
42
+ values(data: unknown): DrizzleInsertValuesChain;
43
+ }
44
+ export interface AnyDrizzleDB {
45
+ select(fields?: Record<string, AnyColumn | SQL>): DrizzleFromChain;
46
+ insert(table: Table): DrizzleInsertChain;
47
+ update(table: Table): DrizzleUpdateChain;
48
+ delete(table: Table): DrizzleDeleteChain;
49
+ }
50
+ export interface DrizzleAdapterConfig {
51
+ /**
52
+ * Primary-key field name. Defaults to auto-detecting the first column with `.primaryKey()`.
53
+ * Set explicitly when using composite PKs or a non-standard naming convention.
54
+ */
55
+ idField?: string;
56
+ }
57
+ /**
58
+ * Drizzle ORM repository adapter for `@edium/halifax`.
59
+ *
60
+ * Install `drizzle-orm` as a peer dependency alongside your preferred Drizzle driver
61
+ * (`drizzle-orm/better-sqlite3`, `drizzle-orm/postgres-js`, `drizzle-orm/mysql2`, etc.)
62
+ * then pass the `db` instance and your table schema:
63
+ *
64
+ * ```ts
65
+ * import { DrizzleAdapter } from '@edium/halifax/drizzle'
66
+ * import { drizzle } from 'drizzle-orm/postgres-js'
67
+ * import postgres from 'postgres'
68
+ * import { usersTable } from './schema'
69
+ *
70
+ * const db = drizzle(postgres(process.env.DATABASE_URL!))
71
+ *
72
+ * const usersResource: ResourceDefinition = {
73
+ * routePrefix: 'users',
74
+ * repository: new DrizzleAdapter(db, usersTable),
75
+ * }
76
+ * ```
77
+ *
78
+ * Field schema and types are inferred automatically from the Drizzle table definition.
79
+ * Multi-tenant isolation via `withScope()` and the advanced query builder via
80
+ * `executeQuery()` are both supported.
81
+ *
82
+ * @template TRecord - Shape of the records returned from the database.
83
+ * @template TCreate - Shape of the data used for inserts (defaults to `Partial<TRecord>`).
84
+ * @template TUpdate - Shape of the data used for updates (defaults to `Partial<TRecord>`).
85
+ */
86
+ export declare class DrizzleAdapter<TRecord = Record<string, unknown>, TCreate = Partial<TRecord>, TUpdate = Partial<TRecord>> implements Repository<TRecord, TCreate, TUpdate> {
87
+ private readonly db;
88
+ private readonly table;
89
+ readonly fields: FieldDefinition[];
90
+ readonly idField: string;
91
+ private readonly columns;
92
+ private readonly scope;
93
+ constructor(db: AnyDrizzleDB, table: Table, config?: DrizzleAdapterConfig, scope?: TenantScope | null);
94
+ private detectPrimaryKey;
95
+ /**
96
+ * Derives a Halifax field schema from a Drizzle table definition.
97
+ * Column types are mapped to their OpenAPI equivalents automatically.
98
+ * @param table - The Drizzle table schema.
99
+ * @param idField - The primary-key field name (auto-detected when omitted).
100
+ * @returns The resolved field definition list.
101
+ */
102
+ static fieldsFromTable(table: Table, idField?: string): FieldDefinition[];
103
+ /**
104
+ * Returns a dynamic SELECT builder so the chain type stays stable across `.where()`,
105
+ * `.orderBy()`, `.limit()`, and `.offset()` calls — Drizzle's `.$dynamic()` opts out
106
+ * of the progressive-omit type narrowing.
107
+ */
108
+ private buildSelect;
109
+ private listWhereToSQL;
110
+ private withScopeWhere;
111
+ private stripScope;
112
+ getOne(id: string | number, options?: Pick<ListOptions, 'fields' | 'include'>): Promise<TRecord | null>;
113
+ getMany(options?: ListOptions): Promise<ListResult<TRecord>>;
114
+ createOne(data: TCreate, _options?: {
115
+ idempotencyKey?: string;
116
+ }): Promise<TRecord>;
117
+ createMany(data: TCreate[], _options?: {
118
+ idempotencyKey?: string;
119
+ }): Promise<TRecord[]>;
120
+ updateOne(id: string | number, data: TUpdate): Promise<TRecord | null>;
121
+ upsertOne(id: string | number, data: TCreate & TUpdate): Promise<TRecord>;
122
+ updateMany(query: IQueryOptions, data: TUpdate): Promise<UpdateManyResult<TRecord>>;
123
+ deleteOne(id: string | number): Promise<boolean>;
124
+ deleteMany(query: IQueryOptions): Promise<DeleteManyResult>;
125
+ executeQuery(query: IQueryOptions): Promise<QueryResult<TRecord>>;
126
+ withScope(scope: TenantScope): Repository<TRecord, TCreate, TUpdate>;
127
+ }
128
+ export {};
@@ -0,0 +1,255 @@
1
+ import { count, eq, getTableColumns, and, inArray, asc, desc } from 'drizzle-orm';
2
+ import { astToDrizzleWhere, astToDrizzleOrderBy } from './astToDrizzle.js';
3
+ function drizzleTypeToOpenApi(col) {
4
+ switch (col.dataType) {
5
+ case 'string':
6
+ return { type: 'string' };
7
+ case 'number':
8
+ return { type: 'number' };
9
+ case 'boolean':
10
+ return { type: 'boolean' };
11
+ case 'bigint':
12
+ return { type: 'integer', format: 'int64' };
13
+ case 'date':
14
+ return { type: 'string', format: 'date-time' };
15
+ case 'json':
16
+ return { type: 'object' };
17
+ case 'buffer':
18
+ return { type: 'string', format: 'binary' };
19
+ default:
20
+ return {};
21
+ }
22
+ }
23
+ /**
24
+ * Drizzle ORM repository adapter for `@edium/halifax`.
25
+ *
26
+ * Install `drizzle-orm` as a peer dependency alongside your preferred Drizzle driver
27
+ * (`drizzle-orm/better-sqlite3`, `drizzle-orm/postgres-js`, `drizzle-orm/mysql2`, etc.)
28
+ * then pass the `db` instance and your table schema:
29
+ *
30
+ * ```ts
31
+ * import { DrizzleAdapter } from '@edium/halifax/drizzle'
32
+ * import { drizzle } from 'drizzle-orm/postgres-js'
33
+ * import postgres from 'postgres'
34
+ * import { usersTable } from './schema'
35
+ *
36
+ * const db = drizzle(postgres(process.env.DATABASE_URL!))
37
+ *
38
+ * const usersResource: ResourceDefinition = {
39
+ * routePrefix: 'users',
40
+ * repository: new DrizzleAdapter(db, usersTable),
41
+ * }
42
+ * ```
43
+ *
44
+ * Field schema and types are inferred automatically from the Drizzle table definition.
45
+ * Multi-tenant isolation via `withScope()` and the advanced query builder via
46
+ * `executeQuery()` are both supported.
47
+ *
48
+ * @template TRecord - Shape of the records returned from the database.
49
+ * @template TCreate - Shape of the data used for inserts (defaults to `Partial<TRecord>`).
50
+ * @template TUpdate - Shape of the data used for updates (defaults to `Partial<TRecord>`).
51
+ */
52
+ export class DrizzleAdapter {
53
+ db;
54
+ table;
55
+ fields;
56
+ idField;
57
+ columns;
58
+ scope;
59
+ constructor(db, table, config = {}, scope = null) {
60
+ this.db = db;
61
+ this.table = table;
62
+ this.columns = getTableColumns(table);
63
+ this.idField = config.idField ?? this.detectPrimaryKey();
64
+ this.fields = DrizzleAdapter.fieldsFromTable(table, this.idField);
65
+ this.scope = scope;
66
+ }
67
+ detectPrimaryKey() {
68
+ for (const [name, col] of Object.entries(this.columns)) {
69
+ if (col.primary)
70
+ return name;
71
+ }
72
+ return 'id';
73
+ }
74
+ /**
75
+ * Derives a Halifax field schema from a Drizzle table definition.
76
+ * Column types are mapped to their OpenAPI equivalents automatically.
77
+ * @param table - The Drizzle table schema.
78
+ * @param idField - The primary-key field name (auto-detected when omitted).
79
+ * @returns The resolved field definition list.
80
+ */
81
+ static fieldsFromTable(table, idField) {
82
+ const cols = getTableColumns(table);
83
+ const pkField = idField ??
84
+ Object.entries(cols).find(([, c]) => c.primary)?.[0] ??
85
+ 'id';
86
+ return Object.entries(cols).map(([name, col]) => ({
87
+ name,
88
+ filterable: true,
89
+ sortable: true,
90
+ selectable: true,
91
+ writable: name !== pkField,
92
+ ...drizzleTypeToOpenApi(col)
93
+ }));
94
+ }
95
+ /**
96
+ * Returns a dynamic SELECT builder so the chain type stays stable across `.where()`,
97
+ * `.orderBy()`, `.limit()`, and `.offset()` calls — Drizzle's `.$dynamic()` opts out
98
+ * of the progressive-omit type narrowing.
99
+ */
100
+ buildSelect(fields) {
101
+ if (fields?.length) {
102
+ const sel = {};
103
+ for (const f of fields) {
104
+ if (this.columns[f])
105
+ sel[f] = this.columns[f];
106
+ }
107
+ return this.db.select(sel).from(this.table).$dynamic();
108
+ }
109
+ return this.db.select().from(this.table).$dynamic();
110
+ }
111
+ listWhereToSQL(where) {
112
+ if (!where || !Object.keys(where).length)
113
+ return undefined;
114
+ const conditions = Object.entries(where)
115
+ .filter(([k]) => this.columns[k])
116
+ .map(([k, v]) => {
117
+ const col = this.columns[k];
118
+ if (v === null)
119
+ return eq(col, null);
120
+ if (typeof v === 'object' && v !== null && 'in' in v) {
121
+ return inArray(col, v.in);
122
+ }
123
+ return eq(col, v);
124
+ });
125
+ if (!conditions.length)
126
+ return undefined;
127
+ return conditions.length === 1 ? conditions[0] : and(...conditions);
128
+ }
129
+ withScopeWhere(inner) {
130
+ if (!this.scope)
131
+ return inner;
132
+ const col = this.columns[this.scope.field];
133
+ if (!col)
134
+ return inner;
135
+ const scopeEq = eq(col, this.scope.value);
136
+ if (!inner)
137
+ return scopeEq;
138
+ return and(scopeEq, inner);
139
+ }
140
+ stripScope(data) {
141
+ if (!this.scope)
142
+ return data;
143
+ const { [this.scope.field]: _ignored, ...rest } = data;
144
+ return rest;
145
+ }
146
+ async getOne(id, options) {
147
+ const idWhere = eq(this.columns[this.idField], id);
148
+ const where = this.withScopeWhere(idWhere);
149
+ const rows = (await this.buildSelect(options?.fields).where(where));
150
+ return rows[0] ?? null;
151
+ }
152
+ async getMany(options) {
153
+ const filterWhere = this.listWhereToSQL(options?.where);
154
+ const where = this.withScopeWhere(filterWhere);
155
+ const countResult = (await this.db
156
+ .select({ count: count() })
157
+ .from(this.table)
158
+ .where(where));
159
+ const total = Number(countResult[0]?.count ?? 0);
160
+ let query = this.buildSelect(options?.fields).where(where);
161
+ if (options?.orderBy?.length) {
162
+ const sorts = options.orderBy
163
+ .filter((s) => this.columns[s.field])
164
+ .map((s) => {
165
+ const col = this.columns[s.field];
166
+ return s.direction === 'desc' ? desc(col) : asc(col);
167
+ });
168
+ if (sorts.length)
169
+ query = query.orderBy(...sorts);
170
+ }
171
+ if (options?.limit != null)
172
+ query = query.limit(options.limit);
173
+ if (options?.offset != null)
174
+ query = query.offset(options.offset);
175
+ const rows = (await query);
176
+ return { count: total, results: rows };
177
+ }
178
+ async createOne(data, _options) {
179
+ const rows = (await this.db
180
+ .insert(this.table)
181
+ .values(this.scope ? { ...data, [this.scope.field]: this.scope.value } : data)
182
+ .returning());
183
+ return rows[0];
184
+ }
185
+ async createMany(data, _options) {
186
+ if (!data.length)
187
+ return [];
188
+ const stamped = this.scope
189
+ ? data.map((d) => ({ ...d, [this.scope.field]: this.scope.value }))
190
+ : data;
191
+ const rows = (await this.db.insert(this.table).values(stamped).returning());
192
+ return rows;
193
+ }
194
+ async updateOne(id, data) {
195
+ const idWhere = eq(this.columns[this.idField], id);
196
+ const where = this.withScopeWhere(idWhere);
197
+ const rows = (await this.db
198
+ .update(this.table)
199
+ .set(this.stripScope(data))
200
+ .where(where)
201
+ .returning());
202
+ return rows[0] ?? null;
203
+ }
204
+ async upsertOne(id, data) {
205
+ const existing = await this.getOne(id);
206
+ if (existing) {
207
+ const updated = await this.updateOne(id, data);
208
+ return updated;
209
+ }
210
+ return this.createOne({ ...data, [this.idField]: id });
211
+ }
212
+ async updateMany(query, data) {
213
+ const where = this.withScopeWhere(astToDrizzleWhere(query.where, this.columns));
214
+ const rows = (await this.db
215
+ .update(this.table)
216
+ .set(this.stripScope(data))
217
+ .where(where)
218
+ .returning());
219
+ const ids = rows.map((r) => r[this.idField]);
220
+ return { updated: ids, results: rows };
221
+ }
222
+ async deleteOne(id) {
223
+ const idWhere = eq(this.columns[this.idField], id);
224
+ const where = this.withScopeWhere(idWhere);
225
+ const rows = await this.db.delete(this.table).where(where).returning();
226
+ return rows.length > 0;
227
+ }
228
+ async deleteMany(query) {
229
+ const where = this.withScopeWhere(astToDrizzleWhere(query.where, this.columns));
230
+ const rows = (await this.db.delete(this.table).where(where).returning());
231
+ const deleted = rows.map((r) => r[this.idField]);
232
+ return { deleted };
233
+ }
234
+ async executeQuery(query) {
235
+ const where = this.withScopeWhere(astToDrizzleWhere(query.where, this.columns));
236
+ const countResult = (await this.db
237
+ .select({ count: count() })
238
+ .from(this.table)
239
+ .where(where));
240
+ const total = Number(countResult[0]?.count ?? 0);
241
+ const sorts = astToDrizzleOrderBy(query.orderBy, this.columns);
242
+ let q = this.buildSelect(query.fields).where(where);
243
+ if (sorts.length)
244
+ q = q.orderBy(...sorts);
245
+ if (query.limit != null)
246
+ q = q.limit(query.limit);
247
+ if (query.offset != null)
248
+ q = q.offset(query.offset);
249
+ const rows = (await q);
250
+ return { count: total, results: rows };
251
+ }
252
+ withScope(scope) {
253
+ return new DrizzleAdapter(this.db, this.table, { idField: this.idField }, scope ?? null);
254
+ }
255
+ }
@@ -0,0 +1,21 @@
1
+ import type { AnyColumn, SQL } from 'drizzle-orm';
2
+ import type { IQueryFilter, ISort } from '@edium/halifax-types';
3
+ export type ColumnMap = Record<string, AnyColumn>;
4
+ /**
5
+ * Translates a validated query-builder WHERE tree into Drizzle SQL conditions.
6
+ *
7
+ * Logical precedence mirrors the Prisma AST compiler: AND binds tighter than OR, so the
8
+ * filter list is split into OR-separated groups of AND-runs.
9
+ *
10
+ * @param where - The validated filter list from {@link IQueryOptions.where}.
11
+ * @param columns - Column map derived from `getTableColumns(table)`.
12
+ * @returns A Drizzle `SQL` condition, or `undefined` when there are no filters.
13
+ */
14
+ export declare function astToDrizzleWhere(where: IQueryFilter[] | undefined, columns: ColumnMap): SQL | undefined;
15
+ /**
16
+ * Converts the AST `orderBy` ({@link ISort}[]) into Drizzle order expressions.
17
+ * @param orderBy - Sort expressions from the query AST.
18
+ * @param columns - Column map derived from `getTableColumns(table)`.
19
+ * @returns An array of Drizzle order expressions (empty when there are no sorts).
20
+ */
21
+ export declare function astToDrizzleOrderBy(orderBy: ISort[] | undefined, columns: ColumnMap): SQL[];
@@ -0,0 +1,121 @@
1
+ import { eq, ne, gt, gte, lt, lte, like, notLike, inArray, notInArray, between, notBetween, isNull, isNotNull, and, or, asc, desc } from 'drizzle-orm';
2
+ import { SqlComparison, SqlOperator, SqlOrder } from '@edium/halifax-types';
3
+ /** Escapes SQL LIKE metacharacters in a literal value so it is treated as a plain string. */
4
+ function escapeLike(value) {
5
+ return value.replace(/[\\%_]/g, '\\$&');
6
+ }
7
+ function comparisonToDrizzle(filter, col) {
8
+ const v1 = filter.value1;
9
+ const v2 = filter.value2;
10
+ const comparison = (filter.comparison?.toUpperCase() ?? '=');
11
+ switch (comparison) {
12
+ case SqlComparison.Equal:
13
+ return eq(col, v1);
14
+ case SqlComparison.NotEqual:
15
+ return ne(col, v1);
16
+ case SqlComparison.GreaterThan:
17
+ return gt(col, v1);
18
+ case SqlComparison.GreaterThanOrEqual:
19
+ return gte(col, v1);
20
+ case SqlComparison.LessThan:
21
+ return lt(col, v1);
22
+ case SqlComparison.LessThanOrEqual:
23
+ return lte(col, v1);
24
+ case SqlComparison.In:
25
+ return inArray(col, (Array.isArray(v1) ? v1 : [v1]));
26
+ case SqlComparison.NotIn:
27
+ return notInArray(col, (Array.isArray(v1) ? v1 : [v1]));
28
+ case SqlComparison.Between:
29
+ return between(col, v1, v2);
30
+ case SqlComparison.NotBetween:
31
+ return notBetween(col, v1, v2);
32
+ case SqlComparison.IsNull:
33
+ return isNull(col);
34
+ case SqlComparison.IsNotNull:
35
+ return isNotNull(col);
36
+ case SqlComparison.Contains:
37
+ return like(col, `%${escapeLike(String(v1 ?? ''))}%`);
38
+ case SqlComparison.StartsWith:
39
+ return like(col, `${escapeLike(String(v1 ?? ''))}%`);
40
+ case SqlComparison.EndsWith:
41
+ return like(col, `%${escapeLike(String(v1 ?? ''))}`);
42
+ case SqlComparison.Like:
43
+ return like(col, String(v1 ?? ''));
44
+ case SqlComparison.NotLike:
45
+ return notLike(col, String(v1 ?? ''));
46
+ default:
47
+ return eq(col, v1);
48
+ }
49
+ }
50
+ function nodeToDrizzle(filter, columns) {
51
+ const col = columns[filter.field];
52
+ if (!col)
53
+ return undefined;
54
+ const self = comparisonToDrizzle(filter, col);
55
+ if (!self)
56
+ return undefined;
57
+ if (filter.children?.length) {
58
+ const childWhere = astToDrizzleWhere(filter.children, columns);
59
+ if (!childWhere)
60
+ return self;
61
+ return filter.operator?.toUpperCase() === SqlOperator.Or
62
+ ? or(self, childWhere)
63
+ : and(self, childWhere);
64
+ }
65
+ return self;
66
+ }
67
+ /**
68
+ * Translates a validated query-builder WHERE tree into Drizzle SQL conditions.
69
+ *
70
+ * Logical precedence mirrors the Prisma AST compiler: AND binds tighter than OR, so the
71
+ * filter list is split into OR-separated groups of AND-runs.
72
+ *
73
+ * @param where - The validated filter list from {@link IQueryOptions.where}.
74
+ * @param columns - Column map derived from `getTableColumns(table)`.
75
+ * @returns A Drizzle `SQL` condition, or `undefined` when there are no filters.
76
+ */
77
+ export function astToDrizzleWhere(where, columns) {
78
+ if (!where?.length)
79
+ return undefined;
80
+ const groups = [[]];
81
+ where.forEach((filter, index) => {
82
+ groups[groups.length - 1].push(nodeToDrizzle(filter, columns));
83
+ // A node with children has consumed its operator to join self↔children; only
84
+ // a childless node's OR starts a new sibling group.
85
+ const joinsWithOr = !filter.children?.length && filter.operator?.toUpperCase() === SqlOperator.Or;
86
+ if (joinsWithOr && index < where.length - 1)
87
+ groups.push([]);
88
+ });
89
+ const andify = (group) => {
90
+ const valid = group.filter(Boolean);
91
+ if (valid.length === 0)
92
+ return undefined;
93
+ if (valid.length === 1)
94
+ return valid[0];
95
+ return and(...valid);
96
+ };
97
+ if (groups.length === 1)
98
+ return andify(groups[0]);
99
+ const orParts = groups.map(andify).filter(Boolean);
100
+ if (orParts.length === 0)
101
+ return undefined;
102
+ if (orParts.length === 1)
103
+ return orParts[0];
104
+ return or(...orParts);
105
+ }
106
+ /**
107
+ * Converts the AST `orderBy` ({@link ISort}[]) into Drizzle order expressions.
108
+ * @param orderBy - Sort expressions from the query AST.
109
+ * @param columns - Column map derived from `getTableColumns(table)`.
110
+ * @returns An array of Drizzle order expressions (empty when there are no sorts).
111
+ */
112
+ export function astToDrizzleOrderBy(orderBy, columns) {
113
+ if (!orderBy?.length)
114
+ return [];
115
+ return orderBy
116
+ .filter((sort) => columns[sort.field])
117
+ .map((sort) => {
118
+ const col = columns[sort.field];
119
+ return sort.order.toUpperCase() === SqlOrder.DESC ? desc(col) : asc(col);
120
+ });
121
+ }
@@ -0,0 +1,4 @@
1
+ export { DrizzleAdapter } from './DrizzleAdapter.js';
2
+ export type { DrizzleAdapterConfig, AnyDrizzleDB } from './DrizzleAdapter.js';
3
+ export { astToDrizzleWhere, astToDrizzleOrderBy } from './astToDrizzle.js';
4
+ export type { ColumnMap } from './astToDrizzle.js';
@@ -0,0 +1,2 @@
1
+ export { DrizzleAdapter } from './DrizzleAdapter.js';
2
+ export { astToDrizzleWhere, astToDrizzleOrderBy } from './astToDrizzle.js';
@@ -1,4 +1,4 @@
1
- import type { IQueryOptions } from '../../../interfaces/IQueryOptions.js';
1
+ import type { IQueryOptions } from '@edium/halifax-types';
2
2
  import type { Repository, RepositoryCapabilities, DeleteManyResult, ListOptions, ListResult, QueryResult, TenantScope, UpdateManyResult } from '../../../core/types.js';
3
3
  import type { FieldDefinition, RelationDefinition, ModelSchema } from '../../../core/types.js';
4
4
  import type { PrismaAdapterOptions } from './types.js';
@@ -10,6 +10,28 @@ function isNotFoundError(error) {
10
10
  error.code === 'P2025');
11
11
  }
12
12
  import { toSelect, toInclude, toOrderBy } from './helpers.js';
13
+ function prismaTypeToOpenApi(prismaType) {
14
+ switch (prismaType) {
15
+ case 'Int':
16
+ return { type: 'integer', format: 'int32' };
17
+ case 'BigInt':
18
+ return { type: 'integer', format: 'int64' };
19
+ case 'Float':
20
+ return { type: 'number', format: 'float' };
21
+ case 'Decimal':
22
+ return { type: 'number', format: 'double' };
23
+ case 'Boolean':
24
+ return { type: 'boolean' };
25
+ case 'DateTime':
26
+ return { type: 'string', format: 'date-time' };
27
+ case 'Json':
28
+ return { type: 'object' };
29
+ case 'Bytes':
30
+ return { type: 'string', format: 'binary' };
31
+ default:
32
+ return {};
33
+ }
34
+ }
13
35
  /**
14
36
  * PrismaAdapter is a generic repository implementation that uses Prisma delegates to perform
15
37
  * database operations. It handles CRUD plus the query-builder/bulk paths by compiling the
@@ -146,7 +168,8 @@ export class PrismaAdapter {
146
168
  name: f.name,
147
169
  filterable: true,
148
170
  sortable: true,
149
- writable: !f.isId && !f.isReadOnly
171
+ writable: !f.isId && !f.isReadOnly,
172
+ ...prismaTypeToOpenApi(f.type)
150
173
  }));
151
174
  }
152
175
  /**
@@ -1,5 +1,4 @@
1
- import type { IQueryFilter } from '../../../interfaces/IQueryFilter.js';
2
- import type { ISort } from '../../../interfaces/ISort.js';
1
+ import type { IQueryFilter, ISort } from '@edium/halifax-types';
3
2
  /** A Prisma `where` fragment — an arbitrary nested object of field conditions and AND/OR/NOT groups. */
4
3
  export type PrismaWhere = Record<string, unknown>;
5
4
  /**
@@ -1,6 +1,4 @@
1
- import { SqlComparison } from '../../../enums/SqlComparison.js';
2
- import { SqlOperator } from '../../../enums/SqlOperator.js';
3
- import { SqlOrder } from '../../../enums/SqlOrder.js';
1
+ import { SqlComparison, SqlOperator, SqlOrder } from '@edium/halifax-types';
4
2
  /**
5
3
  * Splits a `LIKE` pattern into a Prisma string operator based on its `%` wildcards.
6
4
  * `%x%` → `contains`, `x%` → `startsWith`, `%x` → `endsWith`, and a wildcard-free value
@@ -35,7 +35,7 @@ export function toOrderBy(orderBy) {
35
35
  * @returns The route prefix, e.g., 'user-profiles'.
36
36
  */
37
37
  export function toRoutePrefix(modelName) {
38
- const kebab = modelName.replace(/([A-Z])/g, (m, l, i) => (i > 0 ? '-' : '') + l.toLowerCase());
38
+ const kebab = modelName.replace(/([A-Z])/g, (_match, letter, offset) => (offset > 0 ? '-' : '') + letter.toLowerCase());
39
39
  if (kebab.endsWith('y') && !/[aeiou]y$/.test(kebab))
40
40
  return kebab.slice(0, -1) + 'ies';
41
41
  if (/(?:s|x|z|ch|sh)$/.test(kebab))
@@ -5,21 +5,21 @@ import type { ModelSchema, ModelResourceOptions, CrudPermissions, TenantScope }
5
5
  * and properties, but these are the ones that the adapter will use.
6
6
  */
7
7
  export interface PrismaDelegate {
8
- findUnique?(args: any): Promise<any>;
9
- findFirst?(args: any): Promise<any>;
10
- findMany(args?: any): Promise<any[]>;
11
- count(args?: any): Promise<number>;
12
- create(args: any): Promise<any>;
13
- createMany?(args: any): Promise<{
8
+ findUnique?(args: unknown): Promise<unknown>;
9
+ findFirst?(args: unknown): Promise<unknown>;
10
+ findMany(args?: unknown): Promise<unknown[]>;
11
+ count(args?: unknown): Promise<number>;
12
+ create(args: unknown): Promise<unknown>;
13
+ createMany?(args: unknown): Promise<{
14
14
  count: number;
15
15
  }>;
16
- update(args: any): Promise<any>;
17
- updateMany?(args: any): Promise<{
16
+ update(args: unknown): Promise<unknown>;
17
+ updateMany?(args: unknown): Promise<{
18
18
  count: number;
19
19
  }>;
20
- upsert?(args: any): Promise<any>;
21
- delete(args: any): Promise<any>;
22
- deleteMany?(args: any): Promise<{
20
+ upsert?(args: unknown): Promise<unknown>;
21
+ delete(args: unknown): Promise<unknown>;
22
+ deleteMany?(args: unknown): Promise<{
23
23
  count: number;
24
24
  }>;
25
25
  }