@edium/halifax 2.2.3 → 2.4.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 (61) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README.md +6 -3
  3. package/README_AUTH.md +2 -2
  4. package/README_AUTOCRUD.md +5 -4
  5. package/README_CACHE.md +6 -0
  6. package/README_CLASSES.md +13 -6
  7. package/README_GRAPHQL.md +352 -0
  8. package/README_INTERFACES.md +19 -14
  9. package/README_MULTITENANCY.md +87 -0
  10. package/README_OPENAPI.md +9 -9
  11. package/README_REPO_ADAPTERS.md +10 -0
  12. package/dist/adapters/orm/drizzle/DrizzleAdapter.d.ts +5 -0
  13. package/dist/adapters/orm/drizzle/DrizzleAdapter.js +9 -1
  14. package/dist/adapters/orm/prisma/PrismaAdapter.d.ts +1 -2
  15. package/dist/adapters/orm/prisma/PrismaAdapter.js +106 -34
  16. package/dist/adapters/orm/prisma/astToPrisma.js +6 -0
  17. package/dist/auth/strategies/JwtClaimsAuthStrategy.js +2 -8
  18. package/dist/auth/strategies/PassportStrategies.js +3 -9
  19. package/dist/auth/strategies/types.d.ts +7 -0
  20. package/dist/auth/strategies/types.js +13 -1
  21. package/dist/core/cache/CacheStore.d.ts +12 -0
  22. package/dist/core/cache/createCachingRepository.js +10 -1
  23. package/dist/core/cache/in-memory/InMemoryCacheStore.d.ts +14 -1
  24. package/dist/core/cache/in-memory/InMemoryCacheStore.js +29 -1
  25. package/dist/core/cache/redis/RedisCacheStore.d.ts +6 -0
  26. package/dist/core/cache/redis/RedisCacheStore.js +14 -0
  27. package/dist/core/cache/redis/RedisLikeClient.d.ts +2 -0
  28. package/dist/core/crudRouter.d.ts +38 -0
  29. package/dist/core/crudRouter.js +55 -21
  30. package/dist/core/fields.d.ts +11 -1
  31. package/dist/core/fields.js +19 -0
  32. package/dist/core/handlerUtils.d.ts +7 -1
  33. package/dist/core/handlerUtils.js +15 -11
  34. package/dist/core/handlers/create.js +4 -3
  35. package/dist/core/handlers/deleteMany.js +1 -1
  36. package/dist/core/handlers/deleteOne.js +1 -1
  37. package/dist/core/handlers/query.js +4 -6
  38. package/dist/core/handlers/readMany.js +4 -6
  39. package/dist/core/handlers/readOne.js +4 -7
  40. package/dist/core/handlers/updateMany.js +4 -5
  41. package/dist/core/handlers/updateOne.js +1 -1
  42. package/dist/core/handlers/upsertOne.js +1 -1
  43. package/dist/core/queryString.d.ts +10 -0
  44. package/dist/core/queryString.js +23 -0
  45. package/dist/core/types.d.ts +22 -0
  46. package/dist/core/validation.js +5 -11
  47. package/dist/graphql/graphiql.d.ts +5 -0
  48. package/dist/graphql/graphiql.js +29 -0
  49. package/dist/graphql/index.d.ts +4 -0
  50. package/dist/graphql/index.js +3 -0
  51. package/dist/graphql/registerGraphqlRoute.d.ts +10 -0
  52. package/dist/graphql/registerGraphqlRoute.js +79 -0
  53. package/dist/graphql/scalars.d.ts +6 -0
  54. package/dist/graphql/scalars.js +32 -0
  55. package/dist/graphql/schema.d.ts +3 -0
  56. package/dist/graphql/schema.js +635 -0
  57. package/dist/graphql/types.d.ts +48 -0
  58. package/dist/graphql/types.js +1 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/openapi/specGenerator.js +19 -19
  61. package/package.json +9 -3
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './adapters/http/ExpressAdapter.js';
2
2
  export * from './openapi/index.js';
3
+ export type { GraphQLOptions, GraphQLResourceContext, GraphQLResolverContext } from './graphql/index.js';
3
4
  export * from './adapters/orm/prisma/index.js';
4
5
  export * from './auth/AuthStrategy.js';
5
6
  export * from './core/cache/index.js';
@@ -1,5 +1,5 @@
1
1
  import { defaultCrudPermissions } from '../core/types.js';
2
- import { mergeFieldDefinitions } from '../core/fields.js';
2
+ import { mergeFieldDefinitions, mergeRelationDefinitions, normalizeEnvelope } from '../core/fields.js';
3
3
  // ─── Helpers ──────────────────────────────────────────────────────────────────
4
4
  // Exhaustive map from every FieldType to its JSON Schema type string.
5
5
  // Adding a new FieldType causes a compile error here until the map is updated — no switch to edit.
@@ -21,9 +21,6 @@ function toPascalCase(routePrefix) {
21
21
  .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
22
22
  .join('');
23
23
  }
24
- function normalizeEnvelope(value) {
25
- return typeof value === 'string' && value.length > 0 ? value : null;
26
- }
27
24
  function mergeFields(resource) {
28
25
  const idField = resource.repository?.idField ?? 'id';
29
26
  return mergeFieldDefinitions(resource).map((f) => ({
@@ -31,14 +28,6 @@ function mergeFields(resource) {
31
28
  writable: f.name === idField ? f.writable === true : f.writable !== false
32
29
  }));
33
30
  }
34
- function mergeRelations(resource) {
35
- const byName = new Map();
36
- for (const r of resource.repository?.relations ?? [])
37
- byName.set(r.name, r);
38
- for (const r of resource.relations ?? [])
39
- byName.set(r.name, r);
40
- return [...byName.values()];
41
- }
42
31
  // Wraps a schema under an envelope key if one is active.
43
32
  function withEnvelope(schema, envelope) {
44
33
  if (!envelope)
@@ -202,7 +191,7 @@ const sharedSchemas = {
202
191
  ' { "field": "published", "comparison": "=", "value": true },',
203
192
  ' { "field": "createdAt", "comparison": ">=", "value": "2024-01-01T00:00:00Z" }',
204
193
  ' ],',
205
- ' "orderBy": [{ "field": "createdAt", "direction": "desc" }],',
194
+ ' "orderBy": [{ "field": "createdAt", "order": "DESC" }],',
206
195
  ' "limit": 20,',
207
196
  ' "offset": 0,',
208
197
  ' "fields": ["id", "title", "createdAt"],',
@@ -228,10 +217,10 @@ const sharedSchemas = {
228
217
  description: 'Sort order. Multiple entries produce multi-column sorting.',
229
218
  items: {
230
219
  type: 'object',
231
- required: ['field', 'direction'],
220
+ required: ['field', 'order'],
232
221
  properties: {
233
222
  field: { type: 'string', description: 'Field name to sort by (must be sortable).' },
234
- direction: { type: 'string', enum: ['asc', 'desc'] }
223
+ order: { type: 'string', enum: ['ASC', 'DESC'] }
235
224
  }
236
225
  }
237
226
  },
@@ -240,7 +229,11 @@ const sharedSchemas = {
240
229
  items: { type: 'string' },
241
230
  description: 'Relation names to eagerly load (if the resource supports includes).'
242
231
  },
243
- distinct: { type: 'boolean', description: 'When `true`, de-duplicates results.' }
232
+ distinct: {
233
+ type: 'array',
234
+ items: { type: 'string' },
235
+ description: 'Field names to de-duplicate results on (maps to SQL DISTINCT ON these columns).'
236
+ }
244
237
  }
245
238
  }
246
239
  };
@@ -285,10 +278,17 @@ export function generateOpenApiSpec(resources, options = {}) {
285
278
  : {})
286
279
  }
287
280
  };
281
+ // Note: resources here are the *raw* definitions as passed by the caller — they have NOT
282
+ // been through crudRouter's `normalizeResource()`. That means `mergeFields` and
283
+ // `mergeRelationDefinitions` below re-derive the same merged views that the router already
284
+ // computed at startup. This is intentional: the spec generator is a standalone function
285
+ // (called outside the router for static generation tooling), so it can't rely on the
286
+ // router's normalized state. If crudRouter ever caches normalized resources, pass them
287
+ // here instead to avoid the duplicate merge work.
288
288
  for (const resource of resources) {
289
289
  const permissions = { ...defaultCrudPermissions, ...resource.permissions };
290
290
  const fields = mergeFields(resource);
291
- const relations = mergeRelations(resource);
291
+ const relations = mergeRelationDefinitions(resource);
292
292
  const idField = resource.repository?.idField ?? 'id';
293
293
  const schemaBase = toPascalCase(resource.routePrefix);
294
294
  const tag = resource.name ?? schemaBase;
@@ -502,10 +502,10 @@ export function generateOpenApiSpec(resources, options = {}) {
502
502
  type: 'array',
503
503
  items: {
504
504
  type: 'object',
505
- required: ['field', 'direction'],
505
+ required: ['field', 'order'],
506
506
  properties: {
507
507
  field: { type: 'string' },
508
- direction: { type: 'string', enum: ['asc', 'desc'] }
508
+ order: { type: 'string', enum: ['ASC', 'DESC'] }
509
509
  }
510
510
  }
511
511
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edium/halifax",
3
- "version": "2.2.3",
3
+ "version": "2.4.0",
4
4
  "description": "Auto-generate type-safe REST CRUD APIs from your data models. Adapter-driven: Express/Fastify/HyperExpress, Prisma (PostgreSQL, MySQL, MariaDB, SQL Server, CockroachDB, SQLite), JWT/API-key auth, multi-tenancy, a dynamic query builder, and pluggable Redis caching.",
5
5
  "author": "David LaTour <david@edium.com>",
6
6
  "homepage": "https://github.com/splayfee/halifax#readme",
@@ -56,6 +56,7 @@
56
56
  "README_AUTOCRUD.md",
57
57
  "README_CACHE.md",
58
58
  "README_HTTP_ADAPTERS.md",
59
+ "README_GRAPHQL.md",
59
60
  "README_MULTITENANCY.md",
60
61
  "README_QUERYBUILDER.md",
61
62
  "README_REPO_ADAPTERS.md",
@@ -79,7 +80,8 @@
79
80
  "express": "^4.17.0 || ^5.0.0",
80
81
  "fastify": ">=4.0.0",
81
82
  "hyper-express": ">=6.0.0",
82
- "ultimate-express": ">=2.0.0"
83
+ "ultimate-express": ">=2.0.0",
84
+ "graphql": ">=16.0.0"
83
85
  },
84
86
  "peerDependenciesMeta": {
85
87
  "@prisma/client": {
@@ -99,6 +101,9 @@
99
101
  },
100
102
  "ultimate-express": {
101
103
  "optional": true
104
+ },
105
+ "graphql": {
106
+ "optional": true
102
107
  }
103
108
  },
104
109
  "devDependencies": {
@@ -133,6 +138,7 @@
133
138
  "typescript": "^6.0.3",
134
139
  "typescript-eslint": "^8.61.0",
135
140
  "ultimate-express": "^2.1.1",
141
+ "graphql": "^17.0.0",
136
142
  "vitest": "^4.1.8"
137
143
  },
138
144
  "keywords": [
@@ -166,7 +172,7 @@
166
172
  ],
167
173
  "dependencies": {
168
174
  "uuid": "^14.0.0",
169
- "@edium/halifax-types": "2.2.2"
175
+ "@edium/halifax-types": "2.3.0"
170
176
  },
171
177
  "scripts": {
172
178
  "build": "rm -rf dist && tsc --build && tsc-alias",