hono-crud 0.13.16 → 0.13.17

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.17
4
+
5
+ ### Patch Changes
6
+
7
+ - 2314cf6: Push owner-scoped relation includes (`?include=`) down to the adapter query.
8
+
9
+ The owner-scope filter on relation includes ran as a **post-fetch** filter in the
10
+ core orchestrator: related rows were fetched by foreign key, then cross-tenant /
11
+ soft-deleted ones were dropped before the response. Now the resolved scope (tenant
12
+ column + value, soft-delete column) is threaded into each adapter's `fetchRelated`,
13
+ so the filter is pushed into the **WHERE clause** (drizzle / prisma) or the store
14
+ scan (memory) — the disallowed rows are never fetched. The core orchestrator keeps
15
+ its post-fetch `applyRelationScope` as a defense-in-depth net for adapters that
16
+ ignore the scope argument.
17
+
18
+ Adds the internal `RelationFetchScope` type + `resolveFetchScope`; the
19
+ `FetchRelated` / `SyncFetchRelated` types gain an optional 4th `scope` argument
20
+ (backward-compatible — a 3-arg adapter `fetchRelated` stays assignable). Also adds
21
+ a cross-adapter relation-scoping conformance cell (runs on memory + drizzle; a
22
+ named skip on the prisma leg, whose fixed examples schema has no self-relation).
23
+
3
24
  ## 0.13.16
4
25
 
5
26
  ### Patch Changes
@@ -55,14 +55,28 @@ declare function getRegisteredCrudResources(app: object): readonly RegisteredCru
55
55
 
56
56
  /** A loaded related record (opaque to the orchestrator). */
57
57
  type RelatedRecord = Record<string, unknown>;
58
+ /**
59
+ * The push-down form of {@link RelationConfig.scope} for a single request: the
60
+ * concrete column + value an adapter can add to its related-row query (WHERE) so
61
+ * the owner-scope is enforced in SQL instead of by post-fetch filtering. Computed
62
+ * by {@link resolveFetchScope} from the relation `scope` + the request scope.
63
+ */
64
+ interface RelationFetchScope {
65
+ /** Owner/tenant column on the related table to constrain by equality. */
66
+ tenantField?: string;
67
+ /** The request's tenant id (the equality value for `tenantField`). */
68
+ tenantValue?: unknown;
69
+ /** Soft-delete column on the related table to require `IS NULL`. */
70
+ excludeDeletedField?: string;
71
+ }
58
72
  type ResolveRelation<Handle> = (relationConfig: RelationConfig) => Handle | null | PromiseLike<Handle | null>;
59
- type FetchRelated<Handle> = (handle: Handle, keyField: string, values: unknown[]) => RelatedRecord[] | PromiseLike<RelatedRecord[]>;
73
+ type FetchRelated<Handle> = (handle: Handle, keyField: string, values: unknown[], scope?: RelationFetchScope) => RelatedRecord[] | PromiseLike<RelatedRecord[]>;
60
74
  interface RelationLoaderAdapter<Handle> {
61
75
  resolveRelation: ResolveRelation<Handle>;
62
76
  fetchRelated: FetchRelated<Handle>;
63
77
  }
64
78
  type SyncResolveRelation<Handle> = (relationConfig: RelationConfig) => Handle | null;
65
- type SyncFetchRelated<Handle> = (handle: Handle, keyField: string, values: unknown[]) => RelatedRecord[];
79
+ type SyncFetchRelated<Handle> = (handle: Handle, keyField: string, values: unknown[], scope?: RelationFetchScope) => RelatedRecord[];
66
80
  interface SyncRelationLoaderAdapter<Handle> {
67
81
  resolveRelation: SyncResolveRelation<Handle>;
68
82
  fetchRelated: SyncFetchRelated<Handle>;
@@ -100,4 +114,4 @@ declare function loadRelationsForItemSync<T extends Record<string, unknown>, M e
100
114
  */
101
115
  declare function withIncludableRelations(itemSchema: ZodObject<ZodRawShape>, meta: MetaInput, allowedIncludes: readonly string[]): ZodObject<ZodRawShape>;
102
116
 
103
- export { CrudEndpoints, type FetchRelated, IncludeOptions, MetaInput, type RegisteredCrudResource, type RelatedRecord, RelationConfig, type RelationLoaderAdapter, type ResolveRelation, type SyncFetchRelated, type SyncRelationLoaderAdapter, type SyncResolveRelation, batchLoadRelations, getRegisteredCrudResources, loadRelationsForItem, loadRelationsForItemSync, resolveRelationValueAsync, resolveRelationValueSync, withIncludableRelations };
117
+ export { CrudEndpoints, type FetchRelated, IncludeOptions, MetaInput, type RegisteredCrudResource, type RelatedRecord, RelationConfig, type RelationFetchScope, type RelationLoaderAdapter, type ResolveRelation, type SyncFetchRelated, type SyncRelationLoaderAdapter, type SyncResolveRelation, batchLoadRelations, getRegisteredCrudResources, loadRelationsForItem, loadRelationsForItemSync, resolveRelationValueAsync, resolveRelationValueSync, withIncludableRelations };
package/dist/internal.js CHANGED
@@ -1 +1 @@
1
- export{a as MemoryTtlStore}from'./chunk-YB6AVUPQ.js';export{da as AggregateEndpoint,V as BatchCreateEndpoint,X as BatchDeleteEndpoint,Y as BatchRestoreEndpoint,W as BatchUpdateEndpoint,Z as BatchUpsertEndpoint,_ as BulkPatchEndpoint,a as CRUD_ROUTES,S as CloneEndpoint,M as CreateEndpoint,Q as DeleteEndpoint,pa as ExportEndpoint,qa as ImportEndpoint,R as ListEndpoint,O as ReadEndpoint,T as RestoreEndpoint,fa as SearchEndpoint,P as UpdateEndpoint,U as UpsertEndpoint,ba as VersionCompareEndpoint,$ as VersionHistoryEndpoint,aa as VersionReadEndpoint,ca as VersionRollbackEndpoint,u as applyUpsertRestore,m as buildCursorPage,ea as computeAggregations,l as decodeCursor,k as encodeCursor,sa as getRegisteredCrudResources,ga as searchInMemory,N as withIncludableRelations}from'./chunk-QWMISKGR.js';import'./chunk-P7HU2KIE.js';import'./chunk-H3VBYIDA.js';import'./chunk-CWQSQUV4.js';import'./chunk-SDNXN7M5.js';import'./chunk-HYXDMJ4K.js';import'./chunk-WBHWKOTP.js';import'./chunk-L5CVVJQH.js';export{p as extractBearerToken}from'./chunk-CTOFQ5RC.js';export{p as OpenAPIRoute,c as assertNever,b as isFilterOperator}from'./chunk-A27HDYSF.js';export{a as getClientIp,b as getUserId,e as isPathIncluded,d as matchAny,c as matchPath}from'./chunk-V7ABUFW5.js';export{b as getLogger}from'./chunk-DMGP7QDL.js';export{a as StorageRegistry,b as createStorageFeature}from'./chunk-5P4RVSHT.js';export{a as getContextVar,b as setContextVar}from'./chunk-TLI3TRUA.js';export{b as ApiException,a as CONTEXT_KEYS,j as ConfigurationException,d as NotFoundException,f as UnauthorizedException}from'./chunk-XR6JRDGX.js';import'./chunk-NWOJZP4P.js';function f(e,o,r){let t=o.scope;if(!t||!r)return e;let{tenantField:a,softDeleteField:l}=t,{tenantId:n,includeDeleted:s}=r,d=a!=null&&n!=null,u=l!=null&&!s;return !d&&!u?e:e.filter(c=>!(d&&c[a]!==n||u&&c[l]!=null))}async function ge(e,o,r,t){if(!e.length||!t?.relations?.length||!o.model.relations)return e;let a=e.map(l=>({...l}));for(let l of t.relations){let n=o.model.relations[l];if(!n)continue;let s=await r.resolveRelation(n);s!=null&&(a=await Ee[n.type](a,l,n,s,r,t.scope));}return a}function y(e){return async(o,r,t,a,l,n)=>{let s=t.localKey||"id",d=[...new Set(o.map(p=>p[s]).filter(p=>p!=null))];if(d.length===0)return o.map(p=>({...p,[r]:e?null:[]}));let u=await l.fetchRelated(a,t.foreignKey,d),c=f(u,t,n),i=new Map;for(let p of c){let R=p[t.foreignKey],m=i.get(R);m?m.push(p):i.set(R,[p]);}return o.map(p=>{let R=i.get(p[s])||[];return {...p,[r]:e?R[0]||null:R}})}}function he(){return async(e,o,r,t,a,l)=>{let n=r.localKey||"id",s=[...new Set(e.map(i=>i[r.foreignKey]).filter(i=>i!=null))];if(s.length===0)return e.map(i=>({...i,[o]:null}));let d=await a.fetchRelated(t,n,s),u=f(d,r,l),c=new Map;for(let i of u)c.set(i[n],i);return e.map(i=>({...i,[o]:c.get(i[r.foreignKey])||null}))}}var Ee={hasOne:y(true),hasMany:y(false),belongsTo:he()},x={hasOne:e=>e[0]??null,hasMany:e=>e,belongsTo:e=>e[0]??null};function g(e,o){let r=e.localKey||"id";if(e.type==="belongsTo"){let a=o[e.foreignKey];return a==null?null:{gateValue:a,keyField:r}}let t=o[r];return t==null?null:{gateValue:t,keyField:e.foreignKey}}async function h(e,o,r,t,a){let l=x[o.type],n=g(o,e);if(!n)return l([]);let s=f(await t(r,n.keyField,[n.gateValue]),o,a);return l(s)}function E(e,o,r,t,a){let l=x[o.type],n=g(o,e);if(!n)return l([]);let s=f(t(r,n.keyField,[n.gateValue]),o,a);return l(s)}async function Ce(e,o,r,t){if(!t?.relations?.length||!o.model.relations)return e;let a={...e};for(let l of t.relations){let n=o.model.relations[l];if(!n)continue;let s=await r.resolveRelation(n);s!=null&&(a[l]=await h(a,n,s,r.fetchRelated,t.scope));}return a}function Se(e,o,r,t){if(!t?.relations?.length||!o.model.relations)return e;let a={...e};for(let l of t.relations){let n=o.model.relations[l];if(!n)continue;let s=r.resolveRelation(n);s!=null&&(a[l]=E(a,n,s,r.fetchRelated,t.scope));}return a}export{ge as batchLoadRelations,Ce as loadRelationsForItem,Se as loadRelationsForItemSync,h as resolveRelationValueAsync,E as resolveRelationValueSync};
1
+ export{a as MemoryTtlStore}from'./chunk-YB6AVUPQ.js';export{da as AggregateEndpoint,V as BatchCreateEndpoint,X as BatchDeleteEndpoint,Y as BatchRestoreEndpoint,W as BatchUpdateEndpoint,Z as BatchUpsertEndpoint,_ as BulkPatchEndpoint,a as CRUD_ROUTES,S as CloneEndpoint,M as CreateEndpoint,Q as DeleteEndpoint,pa as ExportEndpoint,qa as ImportEndpoint,R as ListEndpoint,O as ReadEndpoint,T as RestoreEndpoint,fa as SearchEndpoint,P as UpdateEndpoint,U as UpsertEndpoint,ba as VersionCompareEndpoint,$ as VersionHistoryEndpoint,aa as VersionReadEndpoint,ca as VersionRollbackEndpoint,u as applyUpsertRestore,m as buildCursorPage,ea as computeAggregations,l as decodeCursor,k as encodeCursor,sa as getRegisteredCrudResources,ga as searchInMemory,N as withIncludableRelations}from'./chunk-QWMISKGR.js';import'./chunk-P7HU2KIE.js';import'./chunk-H3VBYIDA.js';import'./chunk-CWQSQUV4.js';import'./chunk-SDNXN7M5.js';import'./chunk-HYXDMJ4K.js';import'./chunk-WBHWKOTP.js';import'./chunk-L5CVVJQH.js';export{p as extractBearerToken}from'./chunk-CTOFQ5RC.js';export{p as OpenAPIRoute,c as assertNever,b as isFilterOperator}from'./chunk-A27HDYSF.js';export{a as getClientIp,b as getUserId,e as isPathIncluded,d as matchAny,c as matchPath}from'./chunk-V7ABUFW5.js';export{b as getLogger}from'./chunk-DMGP7QDL.js';export{a as StorageRegistry,b as createStorageFeature}from'./chunk-5P4RVSHT.js';export{a as getContextVar,b as setContextVar}from'./chunk-TLI3TRUA.js';export{b as ApiException,a as CONTEXT_KEYS,j as ConfigurationException,d as NotFoundException,f as UnauthorizedException}from'./chunk-XR6JRDGX.js';import'./chunk-NWOJZP4P.js';function m(e,t,n){let o=t.scope;if(!o||!n)return e;let{tenantField:r,softDeleteField:l}=o,{tenantId:a,includeDeleted:i}=n,p=r!=null&&a!=null,c=l!=null&&!i;return !p&&!c?e:e.filter(u=>!(p&&u[r]!==a||c&&u[l]!=null))}function y(e,t){let n=e.scope;if(!n||!t)return;let o=n.tenantField!=null&&t.tenantId!=null?n.tenantField:void 0,r=n.softDeleteField!=null&&!t.includeDeleted?n.softDeleteField:void 0;if(!(o==null&&r==null))return {tenantField:o,tenantValue:t.tenantId,excludeDeletedField:r}}async function Se(e,t,n,o){if(!e.length||!o?.relations?.length||!t.model.relations)return e;let r=e.map(l=>({...l}));for(let l of o.relations){let a=t.model.relations[l];if(!a)continue;let i=await n.resolveRelation(a);i!=null&&(r=await Ee[a.type](r,l,a,i,n,o.scope));}return r}function g(e){return async(t,n,o,r,l,a)=>{let i=o.localKey||"id",p=[...new Set(t.map(d=>d[i]).filter(d=>d!=null))];if(p.length===0)return t.map(d=>({...d,[n]:e?null:[]}));let c=y(o,a),u=await l.fetchRelated(r,o.foreignKey,p,c),f=m(u,o,a),s=new Map;for(let d of f){let R=d[o.foreignKey],x=s.get(R);x?x.push(d):s.set(R,[d]);}return t.map(d=>{let R=s.get(d[i])||[];return {...d,[n]:e?R[0]||null:R}})}}function Ce(){return async(e,t,n,o,r,l)=>{let a=n.localKey||"id",i=[...new Set(e.map(s=>s[n.foreignKey]).filter(s=>s!=null))];if(i.length===0)return e.map(s=>({...s,[t]:null}));let p=y(n,l),c=await r.fetchRelated(o,a,i,p),u=m(c,n,l),f=new Map;for(let s of u)f.set(s[a],s);return e.map(s=>({...s,[t]:f.get(s[n.foreignKey])||null}))}}var Ee={hasOne:g(true),hasMany:g(false),belongsTo:Ce()},h={hasOne:e=>e[0]??null,hasMany:e=>e,belongsTo:e=>e[0]??null};function S(e,t){let n=e.localKey||"id";if(e.type==="belongsTo"){let r=t[e.foreignKey];return r==null?null:{gateValue:r,keyField:n}}let o=t[n];return o==null?null:{gateValue:o,keyField:e.foreignKey}}async function C(e,t,n,o,r){let l=h[t.type],a=S(t,e);if(!a)return l([]);let i=m(await o(n,a.keyField,[a.gateValue],y(t,r)),t,r);return l(i)}function E(e,t,n,o,r){let l=h[t.type],a=S(t,e);if(!a)return l([]);let i=m(o(n,a.keyField,[a.gateValue],y(t,r)),t,r);return l(i)}async function Fe(e,t,n,o){if(!o?.relations?.length||!t.model.relations)return e;let r={...e};for(let l of o.relations){let a=t.model.relations[l];if(!a)continue;let i=await n.resolveRelation(a);i!=null&&(r[l]=await C(r,a,i,n.fetchRelated,o.scope));}return r}function ke(e,t,n,o){if(!o?.relations?.length||!t.model.relations)return e;let r={...e};for(let l of o.relations){let a=t.model.relations[l];if(!a)continue;let i=n.resolveRelation(a);i!=null&&(r[l]=E(r,a,i,n.fetchRelated,o.scope));}return r}export{Se as batchLoadRelations,Fe as loadRelationsForItem,ke as loadRelationsForItemSync,C as resolveRelationValueAsync,E as resolveRelationValueSync};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-crud",
3
- "version": "0.13.16",
3
+ "version": "0.13.17",
4
4
  "description": "CRUD generator for Hono with Zod validation and OpenAPI generation",
5
5
  "author": "Kauan Guesser <contato@kauan.net>",
6
6
  "license": "MIT",