@stacksjs/orm 0.75.256 → 0.75.258

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.
@@ -7,6 +7,7 @@ import { createSoftDeleteMethods } from './traits/soft-deletes';
7
7
  import { createTaggableMethods } from './traits/taggable';
8
8
  import { createTwoFactorMethods } from './traits/two-factor';
9
9
  import { type OrmModelDefinition as BQBModelDefinition, type OrmModelStatic } from '@stacksjs/query-builder';
10
+ import { type Paginator } from '@stacksjs/pagination';
10
11
  import type { ApiMiddleware, DashboardModelOptions, SearchEngineSearchParams } from '@stacksjs/types';
11
12
  import type { BelongsToForeignKeys } from './model-types';
12
13
  import type { Faker } from '@stacksjs/faker';
@@ -340,8 +341,9 @@ export type ModelSearchStatics<TDef> = TDef extends { traits: { useSearch: infer
340
341
  removeAllFromSearch: () => Promise<void>
341
342
  }
342
343
  : {}
343
- export type StacksModelStatic<TDef extends ModelDefinition> = Omit<QueryModel<TDef>, 'update' | 'delete'> & TDef & TraitMethods & ModelSearchStatics<TDef> & {
344
+ export type StacksModelStatic<TDef extends ModelDefinition> = Omit<QueryModel<TDef>, 'update' | 'delete' | 'paginate'> & TDef & TraitMethods & ModelSearchStatics<TDef> & {
344
345
  readonly [MODEL_DEFINITION]: TDef
346
+ paginate: (perPage?: number, page?: number) => Promise<Paginator<Awaited<ReturnType<QueryModel<TDef>['paginate']>>['data'][number]>>
345
347
  update: (id: number | string, data: ModelWriteData<TDef>) => ReturnType<QueryModel<TDef>['find']>
346
348
  forceUpdate: (id: number | string, data: ModelForceWriteData<TDef>) => ReturnType<QueryModel<TDef>['find']>
347
349
  forceCreate: (data: ModelForceWriteData<TDef>) => ReturnType<QueryModel<TDef>['create']>
@@ -1 +1 @@
1
- import{createModel,registerModel}from"@stacksjs/query-builder";import{log}from"@stacksjs/logging/runtime";import{parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";import{snakeCase}from"@stacksjs/strings";import{AsyncLocalStorage}from"node:async_hooks";import{toCursorPaginator,toPaginator,toSimplePaginator}from"@stacksjs/pagination";import{enrichPaginatorUrls,resolveCursorArgs,resolvePageArgs}from"./paginator-request";import{hasWriteValidators,validateWriteBody}from"./auto-crud";let modelEventsModule;const eventSuppression=new AsyncLocalStorage;function eventsAreSuppressed(){return eventSuppression.getStore()?.suppressed===!0}export function withoutEvents(fn){return Promise.resolve(eventSuppression.run({suppressed:!0},fn))}const validationSuppression=new AsyncLocalStorage;function validationIsSuppressed(){return validationSuppression.getStore()?.suppressed===!0}export function withoutValidation(fn){return Promise.resolve(validationSuppression.run({suppressed:!0},fn))}export const MODEL_DEFINITION=Symbol.for("stacks.orm.modelDefinition");export class ModelValidationError extends Error{status=422;errors;constructor(modelName,errors){const fields=Object.keys(errors).join(", ");super(`${modelName} validation failed: ${fields}`);this.name="ModelValidationError";this.errors=errors}}const jsonParseFailureSeen=new Set,COLUMN_NAME_CACHE_LIMIT=4096,columnNameCache=Object.create(null);let columnNameCacheSize=0;function toColumnName(attribute){const cached=columnNameCache[attribute];if(cached!==void 0)return cached;const column=snakeCase(attribute);if(columnNameCacheSize<COLUMN_NAME_CACHE_LIMIT){columnNameCache[attribute]=column;columnNameCacheSize++}return column}function logJsonParseFailure(raw,err){const preview=typeof raw==="string"?raw.slice(0,80):String(raw),key=`${typeof raw}:${preview}`;if(jsonParseFailureSeen.has(key))return;jsonParseFailureSeen.add(key);const message=err instanceof Error?err.message:String(err);console.error(`[orm] JSON cast failed to parse value (returning null): ${message} - value preview: ${JSON.stringify(preview)}`)}export const builtInCasters={string:{get:(v)=>v!=null?String(v):null,set:(v)=>v!=null?String(v):null},number:{get:(v)=>v!=null?Number(v):null,set:(v)=>v!=null?Number(v):null},integer:{get:(v)=>v!=null?Math.trunc(Number(v)):null,set:(v)=>v!=null?Math.trunc(Number(v)):null},float:{get:(v)=>v!=null?Number.parseFloat(String(v)):null,set:(v)=>v!=null?Number.parseFloat(String(v)):null},boolean:{get:(v)=>v===1||v==="1"||v===!0||v==="true",set:(v)=>v===!0||v===1||v==="1"||v==="true"?1:0},json:{get:(v)=>{if(v==null)return null;if(typeof v==="string")try{return JSON.parse(v)}catch(err){logJsonParseFailure(v,err);return null}return v},set:(v)=>{if(v==null)return null;return typeof v==="string"?v:JSON.stringify(v)}},datetime:{get:(v)=>v==null||v===""?null:v instanceof Date?v:parseSqlDateTime(v),set:(v)=>v instanceof Date?sqlDateTime(v):v},date:{get:(v)=>v==null||v===""?null:v instanceof Date?v:parseSqlDateTime(v),set:(v)=>v instanceof Date?v.toISOString().split("T")[0]:v},array:{get:(v)=>{if(v==null)return[];if(Array.isArray(v))return v;if(typeof v==="string")try{return JSON.parse(v)}catch{return[]}return[]},set:(v)=>{if(v==null)return null;return Array.isArray(v)?JSON.stringify(v):v}}};function resolveCaster(cast){return typeof cast==="string"?builtInCasters[cast]:cast}const resolvedModelCastsByDefinition=new WeakMap;function resolveModelCasts(casts){const attrs=Object.keys(casts),cached=resolvedModelCastsByDefinition.get(casts);if(cached&&cached.length===attrs.length){let unchanged=!0;for(let index=0;index<attrs.length;index++){const attr=attrs[index];if(cached[index]?.attr!==attr||cached[index]?.definition!==casts[attr]){unchanged=!1;break}}if(unchanged)return cached}const resolved=attrs.map((attr)=>{const definition=casts[attr];return{attr,definition,caster:resolveCaster(definition)}});resolvedModelCastsByDefinition.set(casts,resolved);return resolved}function castAttributes(row,casts,direction){if(!row||typeof row!=="object")return row;const result={...row};for(const{attr,caster}of resolveModelCasts(casts))if(attr in result)result[attr]=caster[direction].call(caster,result[attr]);return result}function isModelInstanceInternalKey(key){return key==="_attributes"||key==="_original"||key==="_definition"||key==="_hasSaved"||key==="_relations"}const STACKS_PROXY_TAG=Symbol.for("stacks.modelInstanceProxy");let defaultModelInstanceProxyHandler;const castModelInstanceProxyHandlers=new WeakMap,TRAIT_INSTANCE_METHOD_BINDINGS={messenger:{bag:"_messaging",mode:"id"},tags:{bag:"_taggable",mode:"id"},tagCount:{bag:"_taggable",mode:"id"},addTag:{bag:"_taggable",mode:"id"},activeTags:{bag:"_taggable",mode:"id"},inactiveTags:{bag:"_taggable",mode:"id"},removeTag:{bag:"_taggable",mode:"id"},categories:{bag:"_categorizable",mode:"id"},categoryCount:{bag:"_categorizable",mode:"id"},addCategory:{bag:"_categorizable",mode:"id"},activeCategories:{bag:"_categorizable",mode:"id"},inactiveCategories:{bag:"_categorizable",mode:"id"},removeCategory:{bag:"_categorizable",mode:"id"},comments:{bag:"_commentable",mode:"id"},commentCount:{bag:"_commentable",mode:"id"},addComment:{bag:"_commentable",mode:"id"},approvedComments:{bag:"_commentable",mode:"id"},pendingComments:{bag:"_commentable",mode:"id"},rejectedComments:{bag:"_commentable",mode:"id"},likes:{bag:"_likeable",mode:"id"},likeCount:{bag:"_likeable",mode:"id"},like:{bag:"_likeable",mode:"id"},unlike:{bag:"_likeable",mode:"id"},isLiked:{bag:"_likeable",mode:"id"},...Object.fromEntries(BILLABLE_INSTANCE_METHODS.map((name)=>[name,{bag:"_billable",mode:"model"}])),generateTwoFactorForModel:{bag:"_twoFactor",mode:"model"},verifyTwoFactorCode:{bag:"_twoFactor",mode:"model"}};function resolveRelationPath(relations,path){if(!relations||!path)return null;const segments=path.split(".");let current=relations;for(const segment of segments){if(current==null||typeof current!=="object")return null;const obj=current;if("_attributes"in obj&&obj._attributes&&typeof obj._attributes==="object"){const attrs=obj._attributes;if(segment in attrs){current=attrs[segment];continue}}if(segment in obj){current=obj[segment];continue}return null}return current??null}function keepWrapped(result,raw,wrapped){if(result&&typeof result.then==="function")return result.then((value)=>value===raw?wrapped:value);return result===raw?wrapped:result}function wrapModelInstance(instance,casts){if(!instance||typeof instance!=="object")return instance;const candidate=instance;if(candidate[STACKS_PROXY_TAG])return instance;const attrs=candidate._attributes;if(!attrs||typeof attrs!=="object")return instance;if(casts){for(const{attr,caster}of resolveModelCasts(casts))if(Object.hasOwn(attrs,attr))attrs[attr]=caster.get.call(caster,attrs[attr])}let handler=casts?castModelInstanceProxyHandlers.get(casts):defaultModelInstanceProxyHandler;if(!handler){handler={get(rawTarget,prop,recv){const target=rawTarget;if(prop===STACKS_PROXY_TAG)return!0;if(prop==="saveAsync")return async function(){const def=target._definition,setters=def?.set;if(setters&&typeof target.isDirty==="function"){for(const[key,fn]of Object.entries(setters)){if(typeof fn!=="function")continue;if(!target.isDirty(key))continue;const result=fn(target._attributes),value=result&&typeof result.then==="function"?await result:result;target._attributes[key]=value}const original=def.set;def.set={};try{return keepWrapped(target.save(),rawTarget,recv)}finally{def.set=original}}return keepWrapped(target.save(),rawTarget,recv)};if(prop==="save")return function(){const def=target._definition,setters=def?.set;if(setters&&Object.keys(setters).length>0&&typeof target.isDirty==="function"){for(const[key,fn]of Object.entries(setters)){if(typeof fn!=="function")continue;if(!target.isDirty(key))continue;const result=fn(target._attributes);if(result&&typeof result.then==="function")throw Error(`Setter for "${key}" returned a Promise - use \`saveAsync()\` instead of \`save()\` when a model has async setters.`);target._attributes[key]=result}const original=def.set;def.set={};try{return keepWrapped(target.save(),rawTarget,recv)}finally{def.set=original}}return keepWrapped(target.save(),rawTarget,recv)};if(prop==="updateAsync")return async function(data){if(data&&typeof data==="object"&&!Array.isArray(data))for(const[k,v]of Object.entries(data))recv[k]=v;return recv.saveAsync()};if(prop==="saveQuietly")return function(){return withoutEvents(()=>target.save())};if(prop==="saveAsyncQuietly")return function(){return withoutEvents(()=>recv.saveAsync())};if(prop==="updateQuietly")return function(data){return withoutEvents(()=>target.update(data))};if(prop==="updateAsyncQuietly")return function(data){return withoutEvents(()=>recv.updateAsync(data))};if(prop==="deleteQuietly")return function(){return withoutEvents(()=>target.delete())};if(prop==="toSearchableObject")return function toSearchableObject(){const search=target._definition?.traits?.useSearch;if(!search)return null;const attrs=target._attributes;if(!attrs)return null;if(search===!0){const doc={...attrs};if(attrs.id!=null)doc.id=String(attrs.id);return doc}const doc={},keys=search.displayable?.length?search.displayable:[...search.searchable??[],...search.filterable??[],...search.sortable??[],"id"],denormalize=search.denormalize??{},relations=target._relations;for(const key of keys){const snake=snakeCase(key),own=attrs[snake]??attrs[key];if(own!==void 0){doc[snake]=own;continue}const path=denormalize[key]??denormalize[snake];if(path){doc[snake]=resolveRelationPath(relations,path);continue}doc[snake]=void 0}if(attrs.id!=null)doc.id=String(attrs.id);return doc};if(typeof prop==="string"&&!isModelInstanceInternalKey(prop)){const a=target._attributes;if(a&&Object.hasOwn(a,prop))return a[prop];const rels=target._relations;if(rels&&Object.hasOwn(rels,prop)){const related=rels[prop];if(Array.isArray(related))return related.map((x)=>wrapModelInstance(x,casts));return related&&typeof related==="object"?wrapModelInstance(related,casts):related}const binding=TRAIT_INSTANCE_METHOD_BINDINGS[prop];if(binding){const fn=target._definition?.__traitMethods?.[binding.bag]?.[prop];if(typeof fn==="function")return binding.mode==="model"?(...args)=>fn(recv,...args):(...args)=>fn(target.id,...args)}if(a){const column=toColumnName(prop);if(column!==prop&&Object.hasOwn(a,column))return a[column]}}const v=Reflect.get(target,prop,target);return typeof v==="function"?v.bind(target):v},set(rawTarget,prop,value,recv){const target=rawTarget;if(typeof prop==="string"&&!isModelInstanceInternalKey(prop)){const{_attributes:a,set:setter}=target;if(a&&Object.hasOwn(a,prop)){if(typeof setter==="function")setter.call(target,prop,value);else a[prop]=value;return!0}if(a){const column=toColumnName(prop);if(column!==prop&&Object.hasOwn(a,column)){if(typeof setter==="function")setter.call(target,column,value);else a[column]=value;return!0}}if(a&&!(prop in target)){if(typeof setter==="function")setter.call(target,prop,value);else a[prop]=value;return!0}}return Reflect.set(target,prop,value,recv)},has(rawTarget,prop){const target=rawTarget;if(typeof prop==="string"&&!isModelInstanceInternalKey(prop)){const a=target._attributes;if(a&&Object.hasOwn(a,prop))return!0;const rels=target._relations;if(rels&&Object.hasOwn(rels,prop))return!0;if(a){const column=toColumnName(prop);if(column!==prop&&Object.hasOwn(a,column))return!0}}return Reflect.has(target,prop)},deleteProperty(rawTarget,prop){const target=rawTarget;if(typeof prop==="string"&&!isModelInstanceInternalKey(prop)){const a=target._attributes;if(a&&Object.hasOwn(a,prop)){delete a[prop];return!0}}return Reflect.deleteProperty(target,prop)},ownKeys(rawTarget){const a=rawTarget._attributes;return a?Object.keys(a):[]},getOwnPropertyDescriptor(rawTarget,prop){const target=rawTarget;if(typeof prop==="string"){const a=target._attributes;if(a&&Object.hasOwn(a,prop))return{configurable:!0,enumerable:!0,value:a[prop],writable:!0}}return Object.getOwnPropertyDescriptor(target,prop)}};if(casts)castModelInstanceProxyHandlers.set(casts,handler);else defaultModelInstanceProxyHandler=handler}return new Proxy(instance,handler)}const QB_TERMINATORS=Object.assign(Object.create(null),{get:!0,first:!0,last:!0,firstOrFail:!0,find:!0,findOrFail:!0,all:!0,paginate:!0,simplePaginate:!0,cursorPaginate:!0}),PAGINATE_ADAPTERS={paginate:toPaginator,simplePaginate:toSimplePaginator,cursorPaginate:toCursorPaginator},STACKS_QB_PROXY_TAG=Symbol.for("stacks.queryBuilderProxy");function finalizeQueryResult(result,propName,casts){if(QB_TERMINATORS[propName]){if(Array.isArray(result))return result.map((item)=>wrapModelInstance(item,casts));const adapter=PAGINATE_ADAPTERS[propName];if(adapter&&result&&Array.isArray(result.data)){const wrappedData=result.data.map((item)=>wrapModelInstance(item,casts)),canonical=adapter({...result,data:wrappedData});return enrichPaginatorUrls(canonical)}if(result&&Array.isArray(result.data))return{...result,data:result.data.map((item)=>wrapModelInstance(item,casts))};return wrapModelInstance(result,casts)}if(result&&typeof result==="object"&&typeof result.get==="function")return wrapQueryBuilder(result,casts);return result}function wrapQueryBuilder(qb,casts){if(!qb||typeof qb!=="object")return qb;if(qb[STACKS_QB_PROXY_TAG])return qb;return new Proxy(qb,{get(target,prop,recv){if(prop===STACKS_QB_PROXY_TAG)return!0;const v=Reflect.get(target,prop,recv);if(typeof v!=="function")return v;const propName=String(prop);return function(...args){let callArgs=args;if(propName==="paginate"||propName==="simplePaginate"){const{perPage,page}=resolvePageArgs(args[0],args[1]);callArgs=[perPage,page]}else if(propName==="cursorPaginate"){const{perPage,cursor}=resolveCursorArgs(args[0],args[1]);callArgs=[perPage,cursor,args[2],args[3]]}const result=v.apply(target,callArgs);if(!QB_TERMINATORS[propName]&&result===target)return recv;if(result&&typeof result.then==="function")return result.then((resolved)=>finalizeQueryResult(resolved,propName,casts));return finalizeQueryResult(result,propName,casts)}}})}function wrapReadsWithProxy(baseModel,casts){const applyReadResult=(result)=>Array.isArray(result)?result.map((item)=>wrapModelInstance(item,casts)):wrapModelInstance(result,casts),directReads=["find","first","last","all","firstOrFail","findOrFail","findMany"];for(const method of directReads){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=function(...args){const result=original.apply(this,args);if(result&&typeof result.then==="function")return result.then(applyReadResult);return applyReadResult(result)}}const writeReturningInstance=["create","createMany","firstOrCreate","updateOrCreate","make"];for(const method of writeReturningInstance){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=function(...args){const result=original.apply(this,args);if(result&&typeof result.then==="function")return result.then(applyReadResult);return applyReadResult(result)}}const queryBuilderEntrypoints=["query","where","orWhere","whereIn","whereNotIn","whereNull","whereNotNull","whereLike","whereBetween","whereNotBetween","orderBy","orderByDesc","select","with","limit","take","skip","latest","oldest"];for(const method of queryBuilderEntrypoints){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=function(...args){const qb=original.apply(this,args);return wrapQueryBuilder(qb,casts)}}}export class ModelNotFoundError extends Error{model;id;constructor(model,id){super(id!=null?`[ORM] ${model} not found for id=${String(id)}`:`[ORM] No matching ${model} row`);this.name="ModelNotFoundError";this.model=model;this.id=id}}export class MassAssignmentException extends Error{model;attribute;reason;constructor(model,attribute,reason){const why=reason==="guarded"?`'${attribute}' is marked guarded`:`'${attribute}' is not in the fillable allowlist`;super(`[ORM] Mass assignment to ${model} forbidden: ${why}. Use ${model}.forceCreate(...) / .forceUpdate(...) to bypass.`);this.name="MassAssignmentException";this.model=model;this.attribute=attribute;this.reason=reason}}const MASS_ASSIGNMENT_SYSTEM_COLUMNS=new Set(["id","created_at","updated_at","deleted_at","uuid"]),massAssignmentRuleSets=new WeakMap;function resolveMassAssignmentRuleSet(attrs){const cached=massAssignmentRuleSets.get(attrs);if(cached)return cached;const declared=new Set,fillable=new Set,guarded=new Set;for(const[key,attribute]of Object.entries(attrs)){const column=toColumnName(key);declared.add(column);if(attribute?.fillable===!0)fillable.add(column);if(attribute?.guarded===!0)guarded.add(column)}const rules={allowed:fillable.size>0?fillable:declared,guarded};massAssignmentRuleSets.set(attrs,rules);return rules}function applyMassAssignmentRules(definition,data,options={}){if(options.force)return data;if(!data||typeof data!=="object"||Array.isArray(data))return data;const attrs=definition.attributes;if(!attrs)return data;const{allowed,guarded}=resolveMassAssignmentRuleSet(attrs);for(const key in data){if(!Object.hasOwn(data,key))continue;const col=toColumnName(key);if(MASS_ASSIGNMENT_SYSTEM_COLUMNS.has(col))continue;if(guarded.has(col))throw new MassAssignmentException(definition.name,key,"guarded");if(col.endsWith("_id"))continue;if(!allowed.has(col))throw new MassAssignmentException(definition.name,key,"not-fillable")}return data}async function applyDefinedSetters(definition,data){const setters=definition.set;if(!setters||typeof setters!=="object")return data;const out={...data};for(const[key,fn]of Object.entries(setters)){if(typeof fn!=="function"||!(key in out))continue;try{out[key]=await fn(out)}catch(err){log.error(`[orm] ${definition.name}.set.${key} threw - skipping setter`,err)}}return out}function addStaticHelpers(baseModel,definition){const pk=definition.primaryKey||"id",unwrappedCreate=baseModel.create;function getWhere(method){const w=baseModel.where;if(typeof w!=="function")throw Error(`[ORM] ${definition.name}.${method} needs a working where(): the underlying query builder did not expose one`);return w}function getFind(method){const f=baseModel.find;if(typeof f!=="function")throw Error(`[ORM] ${definition.name}.${method} needs a working find(): the underlying query builder did not expose one`);return f}function getCreate(method){const c=baseModel.create;if(typeof c!=="function")throw Error(`[ORM] ${definition.name}.${method} cannot create: the underlying query builder did not expose create()`);return c}baseModel.update=async function(id,data){if(id==null)throw Error(`[ORM] ${definition.name}.update requires an id as the first argument`);if(!data||typeof data!=="object"||Array.isArray(data))throw Error(`[ORM] ${definition.name}.update requires a data object as the second argument`);if(Object.keys(data).length===0){log.debug(`[orm] ${definition.name}.update called with empty data - short-circuiting and returning current row`);const f=baseModel.find;return typeof f==="function"?await f.call(baseModel,id):null}applyMassAssignmentRules(definition,data);const finalData=await applyDefinedSetters(definition,data);await getWhere("update").call(baseModel,pk,id).update(finalData);const f=baseModel.find;return typeof f==="function"?await f.call(baseModel,id):null};if(typeof baseModel.forceUpdate!=="function")baseModel.forceUpdate=async function(id,data){if(id==null)throw Error(`[ORM] ${definition.name}.forceUpdate requires an id as the first argument`);if(!data||typeof data!=="object"||Array.isArray(data))throw Error(`[ORM] ${definition.name}.forceUpdate requires a data object as the second argument`);const finalData=await applyDefinedSetters(definition,data);await getWhere("forceUpdate").call(baseModel,pk,id).update(finalData);const f=baseModel.find;return typeof f==="function"?await f.call(baseModel,id):null};if(typeof baseModel.forceCreate!=="function")baseModel.forceCreate=async function(data){if(!data||typeof data!=="object"||Array.isArray(data))throw Error(`[ORM] ${definition.name}.forceCreate requires a data object`);const make=baseModel.make;if(typeof make==="function"){const instance=await make.call(baseModel);if(instance&&typeof instance.forceFill==="function"&&typeof instance.save==="function"){instance.forceFill(data);const saved=await instance.save();return saved&&typeof saved==="object"&&saved!==instance&&!saved[STACKS_PROXY_TAG]?instance:saved??instance}}if(typeof unwrappedCreate!=="function")throw Error(`[ORM] ${definition.name}.forceCreate cannot create: the underlying query builder did not expose create()`);return await unwrappedCreate.call(baseModel,data)};baseModel.delete=async function(id){if(id==null)throw Error(`[ORM] ${definition.name}.delete requires an id as the first argument`);if(!await getFind("delete").call(baseModel,id))return!1;await getWhere("delete").call(baseModel,pk,id).delete();return!0};if(typeof baseModel.findOrFail!=="function")baseModel.findOrFail=async function(id){const row=await getFind("findOrFail").call(baseModel,id);if(row==null)throw new ModelNotFoundError(definition.name,id);return row};if(typeof baseModel.exists!=="function")baseModel.exists=async function(id){if(id==null)return!1;const q=getWhere("exists").call(baseModel,pk,id);if(typeof q.count==="function"){const n=await q.count();return Number(n)>0}const f=baseModel.find;return typeof f==="function"?await f.call(baseModel,id)!=null:!1};if(typeof baseModel.firstOrCreate!=="function")baseModel.firstOrCreate=async function(attrs,defaults={}){if(!attrs||typeof attrs!=="object"||Array.isArray(attrs))throw Error(`[ORM] ${definition.name}.firstOrCreate requires a where-attrs object`);const q=getWhere("firstOrCreate").call(baseModel,attrs),existing=typeof q.first==="function"?await q.first():await q;if(existing)return existing;const payload={...attrs,...defaults};applyMassAssignmentRules(definition,payload);return await getCreate("firstOrCreate").call(baseModel,payload)};if(typeof baseModel.updateOrCreate!=="function")baseModel.updateOrCreate=async function(attrs,values){if(!attrs||typeof attrs!=="object"||Array.isArray(attrs))throw Error(`[ORM] ${definition.name}.updateOrCreate requires a where-attrs object`);if(!values||typeof values!=="object"||Array.isArray(values))throw Error(`[ORM] ${definition.name}.updateOrCreate requires a values object`);const q=getWhere("updateOrCreate").call(baseModel,attrs),existing=typeof q.first==="function"?await q.first():await q;if(existing){applyMassAssignmentRules(definition,values);const id=existing[pk];await getWhere("updateOrCreate").call(baseModel,pk,id).update(values);const f=baseModel.find;return typeof f==="function"?await f.call(baseModel,id):{...existing,...values}}const payload={...attrs,...values};applyMassAssignmentRules(definition,payload);return await getCreate("updateOrCreate").call(baseModel,payload)};if(typeof baseModel.count!=="function")baseModel.count=async function(){const q=baseModel;if(typeof q.query==="function"){const builder=q.query();if(typeof builder?.count==="function"){const n=await builder.count();return Number(n)||0}}const{all,get}=baseModel;if(typeof all==="function")return(await all.call(baseModel)||[]).length;if(typeof get==="function")return(await get.call(baseModel)||[]).length;return 0};if(typeof baseModel.pluck!=="function")baseModel.pluck=async function(column){if(!column||typeof column!=="string")throw Error(`[ORM] ${definition.name}.pluck requires a column name`);const all=baseModel.all;return((typeof all==="function"?await all.call(baseModel):[])||[]).map((r)=>r?.[column])};if(typeof baseModel.whereIn!=="function")baseModel.whereIn=async function(column,values){if(!column||typeof column!=="string")throw Error(`[ORM] ${definition.name}.whereIn requires a column name`);if(!Array.isArray(values))throw Error(`[ORM] ${definition.name}.whereIn requires an array of values`);if(values.length===0)return[];const w=baseModel.where;if(typeof w!=="function")return[];const q=w.call(baseModel,column,"in",values);if(q&&typeof q.get==="function")return await q.get();return await q};const orderHelpers=[["latest","desc"],["oldest","asc"]];for(const[name,dir]of orderHelpers)if(typeof baseModel[name]!=="function")baseModel[name]=async function(column="created_at"){const q=baseModel,builder=typeof q.query==="function"?q.query():q;if(typeof builder?.orderBy==="function"){const ordered=builder.orderBy(column,dir);if(typeof ordered.first==="function")return await ordered.first();if(typeof ordered.get==="function")return(await ordered.get())?.[0]??null}const all=baseModel.all;if(typeof all!=="function")return null;return[...await all.call(baseModel)||[]].sort((a,b)=>{const av=a?.[column],bv=b?.[column];if(av===bv)return 0;return(av>bv?1:-1)*(dir==="desc"?-1:1)})[0]??null};const searchConfig=resolveSearchConfig(definition.traits?.useSearch,definition);if(searchConfig){const indexName=searchConfig.index??definition.table??snakeCase(`${definition.name}s`);if(typeof baseModel.search!=="function")baseModel.search=function(query,params){return createSearchQueryBuilder(baseModel,indexName,query,params,searchConfig.searchable)};if(typeof baseModel.makeAllSearchable!=="function")baseModel.makeAllSearchable=async function(chunkSize=500){const all=baseModel.all;if(typeof all!=="function")throw Error(`[ORM] ${definition.name}.makeAllSearchable cannot run: the underlying query builder did not expose all()`);const{useSearchEngine}=await import("@stacksjs/search-engine"),engine=useSearchEngine();let total=0;const query=baseModel.query;if(typeof query==="function")try{const q=query.call(baseModel);if(q&&typeof q.chunk==="function"){await q.chunk(chunkSize,async(rows)=>{const docs=await projectDocumentsFromTrait(rows,searchConfig);if(docs.length>0)await engine.addDocuments(indexName,docs);total+=docs.length});return total}}catch{}const rows=await all.call(baseModel)||[],docs=await projectDocumentsFromTrait(rows,searchConfig);if(docs.length>0)await engine.addDocuments(indexName,docs);return docs.length};if(typeof baseModel.removeAllFromSearch!=="function")baseModel.removeAllFromSearch=async function(){const{searchEngineReady}=await import("@stacksjs/search-engine");await(await searchEngineReady()).deleteAllDocuments(indexName)}}}function searchProjectionRecord(document){if(!document||typeof document!=="object"||Array.isArray(document))throw TypeError("[orm/search] A search projection must return a document object");return Object.fromEntries(Object.entries(document))}export async function projectDocumentsFromTrait(models,config){if(models.length===0)return[];if(typeof config.shapeMany==="function"){const docs=await config.shapeMany(models);if(!Array.isArray(docs)||docs.length!==models.length)throw TypeError(`[orm/search] shapeMany returned ${Array.isArray(docs)?docs.length:typeof docs} documents for ${models.length} rows. It must return one document per row, in order, or documents are indexed under the wrong ids.`);return docs.filter(Boolean).map(searchProjectionRecord)}return(await Promise.all(models.map((model)=>projectDocumentFromTrait(model,config)))).filter(Boolean)}function hiddenAttributeNames(definition){const hidden=new Set,attributes=definition.attributes??{};for(const[name,attribute]of Object.entries(attributes))if(attribute?.hidden)hidden.add(name);return hidden}function withoutHidden(document,hidden){if(!document||hidden.size===0)return document;const safe={};for(const[key,value]of Object.entries(document))if(!hidden.has(key))safe[key]=value;return safe}async function projectDocumentFromTrait(model,config){if(typeof config.shape==="function"){const document=await config.shape(model);return document==null?document:searchProjectionRecord(document)}const fromHook=model?.toSearchableObject?.();if(fromHook!==void 0)return fromHook;return withoutHidden(model?._attributes??(typeof model?.toJSON==="function"?model.toJSON():null),config.hidden??new Set)}function createSearchQueryBuilder(_baseModel,indexName,query,initialParams,searchableFields){const defaultQueryBy=searchableFields&&searchableFields.length>0?{query_by:[...searchableFields].join(",")}:{};let params={q:query,...defaultQueryBy,...initialParams??{}};const builder={with(extra){params={...params,...extra};return builder},where(filter){params={...params,filter};return builder},async get(){const{useSearchEngine}=await import("@stacksjs/search-engine");return(await useSearchEngine().search(indexName,params))?.hits??[]},async paginate(perPage,page=1){if(!Number.isInteger(perPage)||perPage<1)throw TypeError(`[orm/search] paginate() needs a positive integer page size, got ${perPage}`);if(!Number.isInteger(page)||page<1)throw TypeError(`[orm/search] paginate() pages are 1-based, got ${page}`);const{useSearchEngine}=await import("@stacksjs/search-engine"),result=await useSearchEngine().search(indexName,{...params,limit:perPage,offset:(page-1)*perPage});return{hits:result.hits??[],total:Number(result.estimatedTotalHits??result.nbHits??result.total??0),page,perPage}},async raw(){const{useSearchEngine}=await import("@stacksjs/search-engine");return await useSearchEngine().search(indexName,params)}};return builder}function encryptedAliases(key){const snake=snakeCase(key);return snake===key?[key]:[key,snake]}async function decryptAttrsInPlace(row,encryptedKeys){if(!row||typeof row!=="object")return;const bag=row._attributes??row;for(const key of encryptedKeys)for(const alias of encryptedAliases(key)){if(!(alias in bag))continue;const value=bag[alias];if(!isEncrypted(value))continue;bag[alias]=await decryptValue(value)}}function wrapReadsWithEncryption(baseModel,encryptedKeys){if(encryptedKeys.length===0)return;const decryptResult=async(r)=>{if(Array.isArray(r)){for(const item of r)await decryptAttrsInPlace(item,encryptedKeys);return r}if(r&&typeof r==="object"&&Array.isArray(r.data)){for(const item of r.data)await decryptAttrsInPlace(item,encryptedKeys);return r}if(r)await decryptAttrsInPlace(r,encryptedKeys);return r},directReads=["find","first","last","all","firstOrFail","findOrFail","findMany"];for(const method of directReads){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=async function(...args){const result=await original.apply(this,args);return await decryptResult(result)}}const writeReturningInstance=["create","createMany","firstOrCreate","updateOrCreate","make"];for(const method of writeReturningInstance){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=async function(...args){const result=await original.apply(this,args);return await decryptResult(result)}}for(const method of["update","forceUpdate"]){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=async function(id,data){const result=await original.call(this,id,data);return await decryptResult(result)}}const terminals=["first","firstOrFail","get","all","find","findOrFail","last","paginate","simplePaginate"],decorateBuilder=(builder)=>{if(!builder||typeof builder!=="object")return builder;const target=builder;if(target.__stacksEncryptedReads)return builder;for(const method of terminals){const original=target[method];if(typeof original!=="function")continue;target[method]=async function(...args){return await decryptResult(await original.apply(this,args))}}for(const method of["where","whereIn","whereNull","whereNotNull","orWhere","orderBy","orderByDesc","limit","offset","with"]){const original=target[method];if(typeof original!=="function")continue;target[method]=function(...args){return decorateBuilder(original.apply(this,args))}}Object.defineProperty(target,"__stacksEncryptedReads",{value:!0,enumerable:!1});return builder};for(const entry of["where","whereIn","whereNull","whereNotNull","orderBy","orderByDesc","limit","query"]){const original=baseModel[entry];if(typeof original!=="function")continue;baseModel[entry]=function(...args){return decorateBuilder(original.apply(this,args))}}}function wrapWritesWithEncryption(baseModel,encryptedKeys){if(encryptedKeys.length===0)return;const encryptArg=async(data)=>{if(!data||typeof data!=="object"||Array.isArray(data))return data;const out={...data};for(const key of encryptedKeys)for(const alias of encryptedAliases(key))if(alias in out)out[alias]=await encryptValue(out[alias]);return out},writeMethods=["create","firstOrCreate","updateOrCreate","forceCreate"],originalCreateMany=baseModel.createMany;if(typeof originalCreateMany==="function")baseModel.createMany=async function(items,...rest){const encrypted=Array.isArray(items)?await Promise.all(items.map(encryptArg)):items;return await originalCreateMany.call(this,encrypted,...rest)};for(const method of writeMethods){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=async function(...args){args[0]=await encryptArg(args[0]);if(args[1])args[1]=await encryptArg(args[1]);return await original.apply(this,args)}}for(const method of["update","forceUpdate"]){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=async function(id,data){const enc=await encryptArg(data);return await original.call(this,id,enc)}}}function wrapWritesWithMassAssignment(baseModel,definition){const origCreate=baseModel.create;if(typeof origCreate==="function")baseModel.create=async function(data,...rest){applyMassAssignmentRules(definition,data);const finalData=await applyDefinedSetters(definition,data);return await origCreate.call(this,finalData,...rest)};const origCreateMany=baseModel.createMany;if(typeof origCreateMany==="function")baseModel.createMany=async function(items,...rest){const finalItems=[];for(const data of items){applyMassAssignmentRules(definition,data);finalItems.push(await applyDefinedSetters(definition,data))}return await origCreateMany.call(this,finalItems,...rest)};for(const method of["firstOrCreate","updateOrCreate"]){const orig=baseModel[method];if(typeof orig!=="function")continue}}function wrapWritesWithValidation(baseModel,definition){if(!hasWriteValidators(definition))return;const modelName=String(definition?.name??baseModel.name??"Model"),check=(data,hook)=>{if(validationIsSuppressed())return;if(!data||typeof data!=="object"||Array.isArray(data))return;const result=validateWriteBody(data,definition,hook);if(!result.valid)throw new ModelValidationError(modelName,result.errors)},origCreate=baseModel.create;if(typeof origCreate==="function")baseModel.create=async function(data,...rest){check(data,"creating");return await origCreate.call(this,data,...rest)};const origCreateMany=baseModel.createMany;if(typeof origCreateMany==="function")baseModel.createMany=async function(items,...rest){if(Array.isArray(items))for(const data of items)check(data,"creating");return await origCreateMany.call(this,items,...rest)};const origUpdate=baseModel.update;if(typeof origUpdate==="function")baseModel.update=async function(id,data,...rest){check(data,"updating");return await origUpdate.call(this,id,data,...rest)}}function wrapQueryMethodsWithCasts(baseModel,casts){const writeMethods=["create","firstOrCreate","updateOrCreate"],originalCreateMany=baseModel.createMany;if(typeof originalCreateMany==="function")baseModel.createMany=async function(items,...rest){const cast=Array.isArray(items)?items.map((item)=>item&&typeof item==="object"&&!Array.isArray(item)?castAttributes(item,casts,"set"):item):items;return await originalCreateMany.call(this,cast,...rest)};for(const method of writeMethods){const original=baseModel[method];if(typeof original==="function")baseModel[method]=async function(...args){log.debug(`[orm] ${method.charAt(0).toUpperCase()+method.slice(1)} ${String(baseModel.name||"unknown")}`,args[0]);if(args[0]&&typeof args[0]==="object"&&!Array.isArray(args[0]))args[0]=castAttributes(args[0],casts,"set");return await original.apply(this,args)}}const origUpdate=baseModel.update;if(typeof origUpdate==="function")baseModel.update=async function(id,data){const cast=data&&typeof data==="object"&&!Array.isArray(data)?castAttributes(data,casts,"set"):data;return await origUpdate.call(this,id,cast)}}import{createMessagingMethods}from"./messaging";import{createTaggableMethods}from"./traits/taggable";import{createCategorizableMethods}from"./traits/categorizable";import{createCommentableMethods}from"./traits/commentable";import{BILLABLE_INSTANCE_METHODS,createBillableMethods}from"./traits/billable";import{createLikeableMethods}from"./traits/likeable";import{createTwoFactorMethods}from"./traits/two-factor";import{createSoftDeleteMethods,resolveSoftDeleteOptions,cascadeSoftDelete}from"./traits/soft-deletes";import{applyAudit,resolveAuditOptions}from"./traits/audit";import{applyActivityLog,resolveActivityLogOptions}from"./traits/activity-log";import{collectEncryptedAttributes,decryptValue,encryptValue,isEncrypted}from"./utils/encrypted";const KNOWN_TRAITS=new Set(["useUuid","useTimestamps","timestampable","useSoftDeletes","softDeletable","useApi","useAuth","authenticatable","useSearch","searchable","useSeeder","seedable","useSocials","billable","observe","useAudit","useActivityLog","prunable","sharding","likeable","taggable","categorizable","useMessaging","commentable","commentables","broadcastOn","broadcastWith","gdpr"]);export function defineModel(definition){log.debug(`[orm] Defining model: ${definition.name} (table: ${definition.table})`);for(const trait of Object.keys(definition.traits??{}))if(!KNOWN_TRAITS.has(trait))log.warn(`[orm] Model '${definition.name}' declares an unknown trait '${trait}'. It does nothing. Known traits: ${[...KNOWN_TRAITS].sort().join(", ")}.`);const observeHooks=buildEventHooks(definition),searchHooks=buildSearchHooks(definition),hooks=mergeModelHooks(observeHooks,searchHooks),traitMethods=buildTraitMethods(definition);definition.__traitMethods=traitMethods;const authenticatable=definition.traits;if((authenticatable?.useAuth||authenticatable?.authenticatable)&&definition.name!=="PersonalAccessToken"){const declared=definition.morphMany;if(!declared||!("tokenable"in declared))definition.morphMany={...declared??{},tokenable:"PersonalAccessToken"}}const defWithHooks=hooks?{...definition,hooks:{...definition.hooks,...hooks}}:definition,baseModel=createModel(defWithHooks);wrapReadsWithProxy(baseModel,definition.casts);addStaticHelpers(baseModel,defWithHooks);wrapWritesWithMassAssignment(baseModel,defWithHooks);if(definition.casts&&Object.keys(definition.casts).length>0)wrapQueryMethodsWithCasts(baseModel,definition.casts);const encryptedKeys=collectEncryptedAttributes(definition);if(encryptedKeys.length>0){wrapWritesWithEncryption(baseModel,encryptedKeys);wrapReadsWithEncryption(baseModel,encryptedKeys)}const softDeleteFlag=definition.traits?.useSoftDeletes;if(softDeleteFlag)applySoftDeletes(baseModel,defWithHooks,softDeleteFlag);const useAuditDecl=definition.traits?.useAudit;if(useAuditDecl){const auditOpts=resolveAuditOptions(useAuditDecl);applyAudit(baseModel,definition.name,definition.primaryKey||"id",auditOpts)}const activityOptions=resolveActivityLogOptions(definition.traits?.useActivityLog);if(activityOptions)applyActivityLog(baseModel,definition.name,definition.primaryKey||"id",activityOptions,[...hiddenAttributeNames(definition)]);wrapWritesWithValidation(baseModel,defWithHooks);const withHelpers=baseModel;withHelpers.withoutValidation=function(fn){return withoutValidation(fn)};withHelpers.withoutEvents=function(fn){return withoutEvents(fn)};for(const method of["create","createMany","update","firstOrCreate","updateOrCreate","forceCreate","forceUpdate","delete","forceDelete","softDelete","restore"]){const orig=baseModel[method];if(typeof orig!=="function")continue;const quietName=`${method}Quietly`;if(typeof baseModel[quietName]==="function")continue;baseModel[quietName]=function(...args){return withoutEvents(()=>orig.apply(baseModel,args))}}const finalModel=Object.assign(baseModel,traitMethods,{...definition,getDefinition:()=>definition,_isStacksModel:!0});registerModel(definition.name,finalModel);Object.defineProperty(finalModel,MODEL_DEFINITION,{value:definition,enumerable:!1,configurable:!0});return finalModel}function buildEventHooks(definition){const observe=definition.traits?.observe;if(!observe)return;const modelName=definition.name.toLowerCase(),toEventPayload=(model)=>{const attributes=model?.attributes;if(!attributes||typeof attributes!=="object")return model;const pk=definition.primaryKey||"id",payload={...attributes};if(payload[pk]===void 0&&model[pk]!==void 0)payload[pk]=model[pk];return payload},dispatchEvent=async(event,data)=>{if(eventsAreSuppressed())return;try{const{dispatch}=modelEventsModule??=await import("@stacksjs/events");await dispatch(event,data)}catch(err){if(err&&typeof err==="object"&&"code"in err&&err.code==="MODULE_NOT_FOUND")return;console.error(`[ORM] Event '${event}' handler error:`,err);if(process.env.STACKS_ORM_EVENT_ERRORS!=="swallow")throw err}},broadcastOnFn=definition.traits?.broadcastOn,broadcastWithFn=definition.traits?.broadcastWith,broadcastingEnabled=typeof broadcastOnFn==="function"&&typeof broadcastWithFn==="function",dispatchBroadcast=async(event,model)=>{if(!broadcastingEnabled)return;if(eventsAreSuppressed())return;try{const channelsRaw=broadcastOnFn(model),channels=Array.isArray(channelsRaw)?channelsRaw:[channelsRaw],payload=broadcastWithFn(model),broadcast=(await import("@stacksjs/realtime").catch(()=>null))?.broadcast;if(typeof broadcast!=="function")return;for(const channel of channels){if(typeof channel!=="string"||channel.length===0)continue;await broadcast(channel,event,payload)}}catch(err){if(err&&typeof err==="object"&&"code"in err&&err.code==="MODULE_NOT_FOUND")return;console.error(`[ORM] Broadcast '${event}' handler error:`,err);if(process.env.STACKS_ORM_BROADCAST_ERRORS==="throw")throw err}},dispatchBeforeEvent=async(event,data)=>{if(eventsAreSuppressed())return!0;let eventsModule;try{eventsModule=modelEventsModule??=await import("@stacksjs/events");const{dispatchAndCollect}=eventsModule,results=await dispatchAndCollect(event,data);if(results.some((result)=>result.ok&&result.value===!1))return!1;const failure=results.find((result)=>!result.ok);if(failure&&!failure.ok)throw failure.error}catch(err){if(!eventsModule&&err&&typeof err==="object"&&"code"in err&&err.code==="MODULE_NOT_FOUND")return!0;console.error(`[ORM] Before-event '${event}' handler error:`,err);if(process.env.STACKS_ORM_EVENT_ERRORS!=="swallow")throw err}return!0},events=observe===!0?["create","update","delete"]:Array.isArray(observe)?observe:[],hooks={};if(events.includes("create")){hooks.beforeCreate=async(model)=>{log.debug(`[orm] Create ${modelName}`,model);if(!await dispatchBeforeEvent(`${modelName}:saving`,model))throw Error(`[ORM] ${modelName}:saving event cancelled the operation`);if(!await dispatchBeforeEvent(`${modelName}:creating`,model))throw Error(`[ORM] ${modelName}:creating event cancelled the operation`)};hooks.afterCreate=async(model)=>{const payload=toEventPayload(model);await dispatchEvent(`${modelName}:created`,payload);await dispatchEvent(`${modelName}:saved`,payload);await dispatchBroadcast(`${modelName}:created`,model)}}if(events.includes("update")){hooks.beforeUpdate=async(model)=>{log.debug(`[orm] Update ${modelName}#${model?.id??"unknown"}`,model);if(!await dispatchBeforeEvent(`${modelName}:saving`,model))throw Error(`[ORM] ${modelName}:saving event cancelled the operation`);if(!await dispatchBeforeEvent(`${modelName}:updating`,model))throw Error(`[ORM] ${modelName}:updating event cancelled the operation`)};hooks.afterUpdate=async(model)=>{const payload=toEventPayload(model);await dispatchEvent(`${modelName}:updated`,payload);await dispatchEvent(`${modelName}:saved`,payload);await dispatchBroadcast(`${modelName}:updated`,model)}}if(events.includes("delete")){hooks.beforeDelete=async(model)=>{log.debug(`[orm] Delete ${modelName}#${model?.id??"unknown"}`);if(!await dispatchBeforeEvent(`${modelName}:deleting`,model))throw Error(`[ORM] ${modelName}:deleting event cancelled the operation`)};hooks.afterDelete=async(model)=>{await dispatchEvent(`${modelName}:deleted`,toEventPayload(model));await dispatchBroadcast(`${modelName}:deleted`,model)}}return hooks}function mergeModelHooks(...sets){const merged={},names=new Set;for(const set of sets){if(!set)continue;for(const name of Object.keys(set))names.add(name)}for(const name of names){const fns=sets.map((s)=>s?.[name]).filter((fn)=>typeof fn==="function");if(!fns.length)continue;merged[name]=async(...args)=>{for(const fn of fns)await fn(...args)}}return Object.keys(merged).length?merged:void 0}function resolveSearchConfig(useSearch,definition){if(!useSearch)return null;const base=useSearch===!0?{}:typeof useSearch==="object"?{...useSearch}:null;if(!base)return null;if(definition)base.hidden=hiddenAttributeNames(definition);return base}function buildSearchHooks(definition){const config=resolveSearchConfig(definition.traits?.useSearch,definition);if(!config)return;const searchConfig=config,indexName=searchConfig.index??definition.table??snakeCase(`${definition.name}s`);async function projectDocument(model){if(typeof searchConfig.shapeMany==="function")try{const[doc]=await projectDocumentsFromTrait([model],searchConfig);return doc}catch(err){log.warn(`[orm/search] shapeMany() threw for ${definition.name}: ${err.message}`);return}if(typeof searchConfig.shape==="function")try{return await projectDocumentFromTrait(model,searchConfig)}catch(err){log.warn(`[orm/search] shape() threw for ${definition.name}: ${err.message}`);return}const fromHook=wrapModelInstance(model).toSearchableObject?.();if(fromHook!==void 0)return fromHook;return model?._attributes??(typeof model?.toJSON==="function"?model.toJSON():null)}const syncDocument=async(model)=>{if(eventsAreSuppressed())return;const doc=await projectDocument(model);if(!doc)return;if(config.queueable){try{const{Jobs}=await import("@stacksjs/queue");await Jobs.dispatch("SyncSearchIndexJob",{index:indexName,op:"upsert",doc})}catch(err){log.warn(`[orm/search] queue dispatch failed for ${definition.name}; falling back to inline: ${err.message}`);await indexInline(indexName,doc,definition.name)}return}await indexInline(indexName,doc,definition.name)};return{afterCreate:syncDocument,afterUpdate:syncDocument,afterDelete:async(model)=>{if(eventsAreSuppressed())return;const id=model?.id??model?._attributes?.id;if(id==null)return;if(config.queueable){try{const{Jobs}=await import("@stacksjs/queue");await Jobs.dispatch("SyncSearchIndexJob",{index:indexName,op:"delete",id:Number(id)})}catch(err){log.warn(`[orm/search] queue dispatch failed for ${definition.name}#${id}; falling back to inline: ${err.message}`);await removeInline(indexName,Number(id),definition.name)}return}await removeInline(indexName,Number(id),definition.name)}}}const searchFailures=new Map;function warnSearchFailure(key,message){const seen=searchFailures.get(key)??0;searchFailures.set(key,seen+1);if(seen===0)log.warn(`[orm/search] ${message}`);else if(seen===9)log.warn(`[orm/search] ${key} is still failing; further identical warnings suppressed for this process.`)}async function indexInline(indexName,doc,modelName){try{const{useSearchEngine}=await import("@stacksjs/search-engine");await useSearchEngine().addDocument(indexName,doc)}catch(err){warnSearchFailure(`index:${modelName}`,`Failed to index ${modelName}: ${err.message}`)}}async function removeInline(indexName,id,modelName){try{const{useSearchEngine}=await import("@stacksjs/search-engine");await useSearchEngine().deleteDocument(indexName,id)}catch(err){warnSearchFailure(`remove:${modelName}`,`Failed to remove ${modelName}#${id}: ${err.message}`)}}function buildTraitMethods(definition){const methods={},{table:tableName,traits}=definition;if(!traits)return methods;if(traits?.useMessaging)methods._messaging=createMessagingMethods(tableName);if(traits.taggable)methods._taggable=createTaggableMethods(tableName);if(traits.categorizable)methods._categorizable=createCategorizableMethods(tableName);if(traits.commentable)methods._commentable=createCommentableMethods(tableName);if(traits.billable)methods._billable=createBillableMethods(tableName);if(traits.likeable){const likeableOpts=typeof traits.likeable==="object"?traits.likeable:void 0;methods._likeable=createLikeableMethods(tableName,likeableOpts)}const useAuth=traits.useAuth||traits.authenticatable;if(typeof useAuth==="object"&&useAuth&&"useTwoFactor"in useAuth&&useAuth.useTwoFactor)methods._twoFactor=createTwoFactorMethods();return methods}function applySoftDeletes(baseModel,definition,traitFlag){const helpers=createSoftDeleteMethods(baseModel,definition.primaryKey||"id"),options=resolveSoftDeleteOptions(traitFlag),parentDef=definition,modelName=definition.name.toLowerCase(),observeOn=definition.traits?.observe!=null&&definition.traits?.observe!==!1,fireRestoring=async(id)=>{if(!observeOn||eventsAreSuppressed())return!0;try{const{dispatchAsync}=modelEventsModule??=await import("@stacksjs/events"),results=await dispatchAsync(`${modelName}:restoring`,{id});if(Array.isArray(results)&&results.some((r)=>r===!1))return!1}catch(err){if(err?.code!=="MODULE_NOT_FOUND")console.error(`[ORM] Before-event '${modelName}:restoring' handler error:`,err)}return!0},fireRestored=async(id)=>{if(!observeOn||eventsAreSuppressed())return;try{const{dispatch}=modelEventsModule??=await import("@stacksjs/events");await dispatch(`${modelName}:restored`,{id})}catch(err){if(err?.code!=="MODULE_NOT_FOUND")console.error(`[ORM] Event '${modelName}:restored' handler error:`,err)}},transactional=process.env.STACKS_ORM_CASCADE_SWALLOW!=="true",softDeleteFn=async(id)=>{if(!options.cascade?.length||!transactional){const ok=await helpers.softDelete(id);if(ok&&options.cascade?.length)await cascadeSoftDelete(parentDef,options,id,"softDelete");return ok}const{db}=await import("@stacksjs/database/runtime");return await db.transaction(async()=>{const ok=await helpers.softDelete(id);if(ok)await cascadeSoftDelete(parentDef,options,id,"softDelete");return ok})},restoreFn=async(id)=>{if(!await fireRestoring(id))return!1;if(!options.cascade?.length||!transactional){const ok=await helpers.restore(id);if(ok){if(options.cascade?.length)await cascadeSoftDelete(parentDef,options,id,"restore");await fireRestored(id)}return ok}const{db}=await import("@stacksjs/database/runtime"),ok=await db.transaction(async()=>{const inner=await helpers.restore(id);if(inner)await cascadeSoftDelete(parentDef,options,id,"restore");return inner});if(ok)await fireRestored(id);return ok};baseModel.softDelete=softDeleteFn;baseModel.restore=restoreFn;baseModel.forceDelete=helpers.forceDelete;baseModel.withTrashed=helpers.withTrashed;baseModel.onlyTrashed=helpers.onlyTrashed;baseModel.delete=softDeleteFn}export function toAttrs(value){if(value==null)return value;if(Array.isArray(value))return value.map((v)=>toAttrs(v));if(typeof value!=="object")return value;const candidate=value;if(typeof candidate.toJSON==="function")return candidate.toJSON();if(candidate._attributes&&typeof candidate._attributes==="object")return candidate._attributes;return value}
1
+ import{createModel,registerModel}from"@stacksjs/query-builder";import{log}from"@stacksjs/logging/runtime";import{parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";import{snakeCase}from"@stacksjs/strings";import{AsyncLocalStorage}from"node:async_hooks";import{toCursorPaginator,toPaginator,toSimplePaginator}from"@stacksjs/pagination";import{enrichPaginatorUrls,resolveCursorArgs,resolvePageArgs}from"./paginator-request";import{hasWriteValidators,validateWriteBody}from"./auto-crud";let modelEventsModule;const eventSuppression=new AsyncLocalStorage;function eventsAreSuppressed(){return eventSuppression.getStore()?.suppressed===!0}export function withoutEvents(fn){return Promise.resolve(eventSuppression.run({suppressed:!0},fn))}const validationSuppression=new AsyncLocalStorage;function validationIsSuppressed(){return validationSuppression.getStore()?.suppressed===!0}export function withoutValidation(fn){return Promise.resolve(validationSuppression.run({suppressed:!0},fn))}export const MODEL_DEFINITION=Symbol.for("stacks.orm.modelDefinition");export class ModelValidationError extends Error{status=422;errors;constructor(modelName,errors){const fields=Object.keys(errors).join(", ");super(`${modelName} validation failed: ${fields}`);this.name="ModelValidationError";this.errors=errors}}const jsonParseFailureSeen=new Set,COLUMN_NAME_CACHE_LIMIT=4096,columnNameCache=Object.create(null);let columnNameCacheSize=0;function toColumnName(attribute){const cached=columnNameCache[attribute];if(cached!==void 0)return cached;const column=snakeCase(attribute);if(columnNameCacheSize<COLUMN_NAME_CACHE_LIMIT){columnNameCache[attribute]=column;columnNameCacheSize++}return column}function logJsonParseFailure(raw,err){const preview=typeof raw==="string"?raw.slice(0,80):String(raw),key=`${typeof raw}:${preview}`;if(jsonParseFailureSeen.has(key))return;jsonParseFailureSeen.add(key);const message=err instanceof Error?err.message:String(err);console.error(`[orm] JSON cast failed to parse value (returning null): ${message} - value preview: ${JSON.stringify(preview)}`)}export const builtInCasters={string:{get:(v)=>v!=null?String(v):null,set:(v)=>v!=null?String(v):null},number:{get:(v)=>v!=null?Number(v):null,set:(v)=>v!=null?Number(v):null},integer:{get:(v)=>v!=null?Math.trunc(Number(v)):null,set:(v)=>v!=null?Math.trunc(Number(v)):null},float:{get:(v)=>v!=null?Number.parseFloat(String(v)):null,set:(v)=>v!=null?Number.parseFloat(String(v)):null},boolean:{get:(v)=>v===1||v==="1"||v===!0||v==="true",set:(v)=>v===!0||v===1||v==="1"||v==="true"?1:0},json:{get:(v)=>{if(v==null)return null;if(typeof v==="string")try{return JSON.parse(v)}catch(err){logJsonParseFailure(v,err);return null}return v},set:(v)=>{if(v==null)return null;return typeof v==="string"?v:JSON.stringify(v)}},datetime:{get:(v)=>v==null||v===""?null:v instanceof Date?v:parseSqlDateTime(v),set:(v)=>v instanceof Date?sqlDateTime(v):v},date:{get:(v)=>v==null||v===""?null:v instanceof Date?v:parseSqlDateTime(v),set:(v)=>v instanceof Date?v.toISOString().split("T")[0]:v},array:{get:(v)=>{if(v==null)return[];if(Array.isArray(v))return v;if(typeof v==="string")try{return JSON.parse(v)}catch{return[]}return[]},set:(v)=>{if(v==null)return null;return Array.isArray(v)?JSON.stringify(v):v}}};function resolveCaster(cast){return typeof cast==="string"?builtInCasters[cast]:cast}const resolvedModelCastsByDefinition=new WeakMap;function resolveModelCasts(casts){const attrs=Object.keys(casts),cached=resolvedModelCastsByDefinition.get(casts);if(cached&&cached.length===attrs.length){let unchanged=!0;for(let index=0;index<attrs.length;index++){const attr=attrs[index];if(cached[index]?.attr!==attr||cached[index]?.definition!==casts[attr]){unchanged=!1;break}}if(unchanged)return cached}const resolved=attrs.map((attr)=>{const definition=casts[attr];return{attr,definition,caster:resolveCaster(definition)}});resolvedModelCastsByDefinition.set(casts,resolved);return resolved}function castAttributes(row,casts,direction){if(!row||typeof row!=="object")return row;const result={...row};for(const{attr,caster}of resolveModelCasts(casts))if(attr in result)result[attr]=caster[direction].call(caster,result[attr]);return result}function isModelInstanceInternalKey(key){return key==="_attributes"||key==="_original"||key==="_definition"||key==="_hasSaved"||key==="_relations"}const STACKS_PROXY_TAG=Symbol.for("stacks.modelInstanceProxy");let defaultModelInstanceProxyHandler;const castModelInstanceProxyHandlers=new WeakMap,TRAIT_INSTANCE_METHOD_BINDINGS={messenger:{bag:"_messaging",mode:"id"},tags:{bag:"_taggable",mode:"id"},tagCount:{bag:"_taggable",mode:"id"},addTag:{bag:"_taggable",mode:"id"},activeTags:{bag:"_taggable",mode:"id"},inactiveTags:{bag:"_taggable",mode:"id"},removeTag:{bag:"_taggable",mode:"id"},categories:{bag:"_categorizable",mode:"id"},categoryCount:{bag:"_categorizable",mode:"id"},addCategory:{bag:"_categorizable",mode:"id"},activeCategories:{bag:"_categorizable",mode:"id"},inactiveCategories:{bag:"_categorizable",mode:"id"},removeCategory:{bag:"_categorizable",mode:"id"},comments:{bag:"_commentable",mode:"id"},commentCount:{bag:"_commentable",mode:"id"},addComment:{bag:"_commentable",mode:"id"},approvedComments:{bag:"_commentable",mode:"id"},pendingComments:{bag:"_commentable",mode:"id"},rejectedComments:{bag:"_commentable",mode:"id"},likes:{bag:"_likeable",mode:"id"},likeCount:{bag:"_likeable",mode:"id"},like:{bag:"_likeable",mode:"id"},unlike:{bag:"_likeable",mode:"id"},isLiked:{bag:"_likeable",mode:"id"},...Object.fromEntries(BILLABLE_INSTANCE_METHODS.map((name)=>[name,{bag:"_billable",mode:"model"}])),generateTwoFactorForModel:{bag:"_twoFactor",mode:"model"},verifyTwoFactorCode:{bag:"_twoFactor",mode:"model"}};function resolveRelationPath(relations,path){if(!relations||!path)return null;const segments=path.split(".");let current=relations;for(const segment of segments){if(current==null||typeof current!=="object")return null;const obj=current;if("_attributes"in obj&&obj._attributes&&typeof obj._attributes==="object"){const attrs=obj._attributes;if(segment in attrs){current=attrs[segment];continue}}if(segment in obj){current=obj[segment];continue}return null}return current??null}function keepWrapped(result,raw,wrapped){if(result&&typeof result.then==="function")return result.then((value)=>value===raw?wrapped:value);return result===raw?wrapped:result}function wrapModelInstance(instance,casts){if(!instance||typeof instance!=="object")return instance;const candidate=instance;if(candidate[STACKS_PROXY_TAG])return instance;const attrs=candidate._attributes;if(!attrs||typeof attrs!=="object")return instance;if(casts){for(const{attr,caster}of resolveModelCasts(casts))if(Object.hasOwn(attrs,attr))attrs[attr]=caster.get.call(caster,attrs[attr])}let handler=casts?castModelInstanceProxyHandlers.get(casts):defaultModelInstanceProxyHandler;if(!handler){handler={get(rawTarget,prop,recv){const target=rawTarget;if(prop===STACKS_PROXY_TAG)return!0;if(prop==="saveAsync")return async function(){const def=target._definition,setters=def?.set;if(setters&&typeof target.isDirty==="function"){for(const[key,fn]of Object.entries(setters)){if(typeof fn!=="function")continue;if(!target.isDirty(key))continue;const result=fn(target._attributes),value=result&&typeof result.then==="function"?await result:result;target._attributes[key]=value}const original=def.set;def.set={};try{return keepWrapped(target.save(),rawTarget,recv)}finally{def.set=original}}return keepWrapped(target.save(),rawTarget,recv)};if(prop==="save")return function(){const def=target._definition,setters=def?.set;if(setters&&Object.keys(setters).length>0&&typeof target.isDirty==="function"){for(const[key,fn]of Object.entries(setters)){if(typeof fn!=="function")continue;if(!target.isDirty(key))continue;const result=fn(target._attributes);if(result&&typeof result.then==="function")throw Error(`Setter for "${key}" returned a Promise - use \`saveAsync()\` instead of \`save()\` when a model has async setters.`);target._attributes[key]=result}const original=def.set;def.set={};try{return keepWrapped(target.save(),rawTarget,recv)}finally{def.set=original}}return keepWrapped(target.save(),rawTarget,recv)};if(prop==="updateAsync")return async function(data){if(data&&typeof data==="object"&&!Array.isArray(data))for(const[k,v]of Object.entries(data))recv[k]=v;return recv.saveAsync()};if(prop==="saveQuietly")return function(){return withoutEvents(()=>target.save())};if(prop==="saveAsyncQuietly")return function(){return withoutEvents(()=>recv.saveAsync())};if(prop==="updateQuietly")return function(data){return withoutEvents(()=>target.update(data))};if(prop==="updateAsyncQuietly")return function(data){return withoutEvents(()=>recv.updateAsync(data))};if(prop==="deleteQuietly")return function(){return withoutEvents(()=>target.delete())};if(prop==="toSearchableObject")return function toSearchableObject(){const search=target._definition?.traits?.useSearch;if(!search)return null;const attrs=target._attributes;if(!attrs)return null;if(search===!0){const doc={...attrs};if(attrs.id!=null)doc.id=String(attrs.id);return doc}const doc={},keys=search.displayable?.length?search.displayable:[...search.searchable??[],...search.filterable??[],...search.sortable??[],"id"],denormalize=search.denormalize??{},relations=target._relations;for(const key of keys){const snake=snakeCase(key),own=attrs[snake]??attrs[key];if(own!==void 0){doc[snake]=own;continue}const path=denormalize[key]??denormalize[snake];if(path){doc[snake]=resolveRelationPath(relations,path);continue}doc[snake]=void 0}if(attrs.id!=null)doc.id=String(attrs.id);return doc};if(typeof prop==="string"&&!isModelInstanceInternalKey(prop)){const a=target._attributes;if(a&&Object.hasOwn(a,prop))return a[prop];const rels=target._relations;if(rels&&Object.hasOwn(rels,prop)){const related=rels[prop];if(Array.isArray(related))return related.map((x)=>wrapModelInstance(x,casts));return related&&typeof related==="object"?wrapModelInstance(related,casts):related}const binding=TRAIT_INSTANCE_METHOD_BINDINGS[prop];if(binding){const fn=target._definition?.__traitMethods?.[binding.bag]?.[prop];if(typeof fn==="function")return binding.mode==="model"?(...args)=>fn(recv,...args):(...args)=>fn(target.id,...args)}if(a){const column=toColumnName(prop);if(column!==prop&&Object.hasOwn(a,column))return a[column]}}const v=Reflect.get(target,prop,target);return typeof v==="function"?v.bind(target):v},set(rawTarget,prop,value,recv){const target=rawTarget;if(typeof prop==="string"&&!isModelInstanceInternalKey(prop)){const{_attributes:a,set:setter}=target;if(a&&Object.hasOwn(a,prop)){if(typeof setter==="function")setter.call(target,prop,value);else a[prop]=value;return!0}if(a){const column=toColumnName(prop);if(column!==prop&&Object.hasOwn(a,column)){if(typeof setter==="function")setter.call(target,column,value);else a[column]=value;return!0}}if(a&&!(prop in target)){if(typeof setter==="function")setter.call(target,prop,value);else a[prop]=value;return!0}}return Reflect.set(target,prop,value,recv)},has(rawTarget,prop){const target=rawTarget;if(typeof prop==="string"&&!isModelInstanceInternalKey(prop)){const a=target._attributes;if(a&&Object.hasOwn(a,prop))return!0;const rels=target._relations;if(rels&&Object.hasOwn(rels,prop))return!0;if(a){const column=toColumnName(prop);if(column!==prop&&Object.hasOwn(a,column))return!0}}return Reflect.has(target,prop)},deleteProperty(rawTarget,prop){const target=rawTarget;if(typeof prop==="string"&&!isModelInstanceInternalKey(prop)){const a=target._attributes;if(a&&Object.hasOwn(a,prop)){delete a[prop];return!0}}return Reflect.deleteProperty(target,prop)},ownKeys(rawTarget){const a=rawTarget._attributes;return a?Object.keys(a):[]},getOwnPropertyDescriptor(rawTarget,prop){const target=rawTarget;if(typeof prop==="string"){const a=target._attributes;if(a&&Object.hasOwn(a,prop))return{configurable:!0,enumerable:!0,value:a[prop],writable:!0}}return Object.getOwnPropertyDescriptor(target,prop)}};if(casts)castModelInstanceProxyHandlers.set(casts,handler);else defaultModelInstanceProxyHandler=handler}return new Proxy(instance,handler)}const QB_TERMINATORS=Object.assign(Object.create(null),{get:!0,first:!0,last:!0,firstOrFail:!0,find:!0,findOrFail:!0,all:!0,paginate:!0,simplePaginate:!0,cursorPaginate:!0}),PAGINATE_ADAPTERS={paginate:toPaginator,simplePaginate:toSimplePaginator,cursorPaginate:toCursorPaginator},STACKS_QB_PROXY_TAG=Symbol.for("stacks.queryBuilderProxy");function finalizeQueryResult(result,propName,casts){if(QB_TERMINATORS[propName]){if(Array.isArray(result))return result.map((item)=>wrapModelInstance(item,casts));const adapter=PAGINATE_ADAPTERS[propName];if(adapter&&result&&Array.isArray(result.data)){const wrappedData=result.data.map((item)=>wrapModelInstance(item,casts)),canonical=adapter({...result,data:wrappedData});return enrichPaginatorUrls(canonical)}if(result&&Array.isArray(result.data))return{...result,data:result.data.map((item)=>wrapModelInstance(item,casts))};return wrapModelInstance(result,casts)}if(result&&typeof result==="object"&&typeof result.get==="function")return wrapQueryBuilder(result,casts);return result}function wrapQueryBuilder(qb,casts){if(!qb||typeof qb!=="object")return qb;if(qb[STACKS_QB_PROXY_TAG])return qb;return new Proxy(qb,{get(target,prop,recv){if(prop===STACKS_QB_PROXY_TAG)return!0;const v=Reflect.get(target,prop,recv);if(typeof v!=="function")return v;const propName=String(prop);return function(...args){let callArgs=args;if(propName==="paginate"){const{perPage,page}=resolvePageArgs(args[0],args[1]);callArgs=[{perPage,page}]}else if(propName==="simplePaginate"){const{perPage,page}=resolvePageArgs(args[0],args[1]);callArgs=[perPage,page]}else if(propName==="cursorPaginate"){const{perPage,cursor}=resolveCursorArgs(args[0],args[1]);callArgs=[perPage,cursor,args[2],args[3]]}const result=v.apply(target,callArgs);if(!QB_TERMINATORS[propName]&&result===target)return recv;if(result&&typeof result.then==="function")return result.then((resolved)=>finalizeQueryResult(resolved,propName,casts));return finalizeQueryResult(result,propName,casts)}}})}function wrapReadsWithProxy(baseModel,casts){const applyReadResult=(result)=>Array.isArray(result)?result.map((item)=>wrapModelInstance(item,casts)):wrapModelInstance(result,casts),directReads=["find","first","last","all","firstOrFail","findOrFail","findMany"];for(const method of directReads){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=function(...args){const result=original.apply(this,args);if(result&&typeof result.then==="function")return result.then(applyReadResult);return applyReadResult(result)}}const writeReturningInstance=["create","createMany","firstOrCreate","updateOrCreate","make"];for(const method of writeReturningInstance){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=function(...args){const result=original.apply(this,args);if(result&&typeof result.then==="function")return result.then(applyReadResult);return applyReadResult(result)}}const queryBuilderEntrypoints=["query","where","orWhere","whereIn","whereNotIn","whereNull","whereNotNull","whereLike","whereBetween","whereNotBetween","orderBy","orderByDesc","select","with","limit","take","skip","latest","oldest"];for(const method of queryBuilderEntrypoints){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=function(...args){const qb=original.apply(this,args);return wrapQueryBuilder(qb,casts)}}if(typeof baseModel.paginate==="function")baseModel.paginate=(...args)=>{return baseModel.query().paginate(...args)}}export class ModelNotFoundError extends Error{model;id;constructor(model,id){super(id!=null?`[ORM] ${model} not found for id=${String(id)}`:`[ORM] No matching ${model} row`);this.name="ModelNotFoundError";this.model=model;this.id=id}}export class MassAssignmentException extends Error{model;attribute;reason;constructor(model,attribute,reason){const why=reason==="guarded"?`'${attribute}' is marked guarded`:`'${attribute}' is not in the fillable allowlist`;super(`[ORM] Mass assignment to ${model} forbidden: ${why}. Use ${model}.forceCreate(...) / .forceUpdate(...) to bypass.`);this.name="MassAssignmentException";this.model=model;this.attribute=attribute;this.reason=reason}}const MASS_ASSIGNMENT_SYSTEM_COLUMNS=new Set(["id","created_at","updated_at","deleted_at","uuid"]),massAssignmentRuleSets=new WeakMap;function resolveMassAssignmentRuleSet(attrs){const cached=massAssignmentRuleSets.get(attrs);if(cached)return cached;const declared=new Set,fillable=new Set,guarded=new Set;for(const[key,attribute]of Object.entries(attrs)){const column=toColumnName(key);declared.add(column);if(attribute?.fillable===!0)fillable.add(column);if(attribute?.guarded===!0)guarded.add(column)}const rules={allowed:fillable.size>0?fillable:declared,guarded};massAssignmentRuleSets.set(attrs,rules);return rules}function applyMassAssignmentRules(definition,data,options={}){if(options.force)return data;if(!data||typeof data!=="object"||Array.isArray(data))return data;const attrs=definition.attributes;if(!attrs)return data;const{allowed,guarded}=resolveMassAssignmentRuleSet(attrs);for(const key in data){if(!Object.hasOwn(data,key))continue;const col=toColumnName(key);if(MASS_ASSIGNMENT_SYSTEM_COLUMNS.has(col))continue;if(guarded.has(col))throw new MassAssignmentException(definition.name,key,"guarded");if(col.endsWith("_id"))continue;if(!allowed.has(col))throw new MassAssignmentException(definition.name,key,"not-fillable")}return data}async function applyDefinedSetters(definition,data){const setters=definition.set;if(!setters||typeof setters!=="object")return data;const out={...data};for(const[key,fn]of Object.entries(setters)){if(typeof fn!=="function"||!(key in out))continue;try{out[key]=await fn(out)}catch(err){log.error(`[orm] ${definition.name}.set.${key} threw - skipping setter`,err)}}return out}function addStaticHelpers(baseModel,definition){const pk=definition.primaryKey||"id",unwrappedCreate=baseModel.create;function getWhere(method){const w=baseModel.where;if(typeof w!=="function")throw Error(`[ORM] ${definition.name}.${method} needs a working where(): the underlying query builder did not expose one`);return w}function getFind(method){const f=baseModel.find;if(typeof f!=="function")throw Error(`[ORM] ${definition.name}.${method} needs a working find(): the underlying query builder did not expose one`);return f}function getCreate(method){const c=baseModel.create;if(typeof c!=="function")throw Error(`[ORM] ${definition.name}.${method} cannot create: the underlying query builder did not expose create()`);return c}baseModel.update=async function(id,data){if(id==null)throw Error(`[ORM] ${definition.name}.update requires an id as the first argument`);if(!data||typeof data!=="object"||Array.isArray(data))throw Error(`[ORM] ${definition.name}.update requires a data object as the second argument`);if(Object.keys(data).length===0){log.debug(`[orm] ${definition.name}.update called with empty data - short-circuiting and returning current row`);const f=baseModel.find;return typeof f==="function"?await f.call(baseModel,id):null}applyMassAssignmentRules(definition,data);const finalData=await applyDefinedSetters(definition,data);await getWhere("update").call(baseModel,pk,id).update(finalData);const f=baseModel.find;return typeof f==="function"?await f.call(baseModel,id):null};if(typeof baseModel.forceUpdate!=="function")baseModel.forceUpdate=async function(id,data){if(id==null)throw Error(`[ORM] ${definition.name}.forceUpdate requires an id as the first argument`);if(!data||typeof data!=="object"||Array.isArray(data))throw Error(`[ORM] ${definition.name}.forceUpdate requires a data object as the second argument`);const finalData=await applyDefinedSetters(definition,data);await getWhere("forceUpdate").call(baseModel,pk,id).update(finalData);const f=baseModel.find;return typeof f==="function"?await f.call(baseModel,id):null};if(typeof baseModel.forceCreate!=="function")baseModel.forceCreate=async function(data){if(!data||typeof data!=="object"||Array.isArray(data))throw Error(`[ORM] ${definition.name}.forceCreate requires a data object`);const make=baseModel.make;if(typeof make==="function"){const instance=await make.call(baseModel);if(instance&&typeof instance.forceFill==="function"&&typeof instance.save==="function"){instance.forceFill(data);const saved=await instance.save();return saved&&typeof saved==="object"&&saved!==instance&&!saved[STACKS_PROXY_TAG]?instance:saved??instance}}if(typeof unwrappedCreate!=="function")throw Error(`[ORM] ${definition.name}.forceCreate cannot create: the underlying query builder did not expose create()`);return await unwrappedCreate.call(baseModel,data)};baseModel.delete=async function(id){if(id==null)throw Error(`[ORM] ${definition.name}.delete requires an id as the first argument`);if(!await getFind("delete").call(baseModel,id))return!1;await getWhere("delete").call(baseModel,pk,id).delete();return!0};if(typeof baseModel.findOrFail!=="function")baseModel.findOrFail=async function(id){const row=await getFind("findOrFail").call(baseModel,id);if(row==null)throw new ModelNotFoundError(definition.name,id);return row};if(typeof baseModel.exists!=="function")baseModel.exists=async function(id){if(id==null)return!1;const q=getWhere("exists").call(baseModel,pk,id);if(typeof q.count==="function"){const n=await q.count();return Number(n)>0}const f=baseModel.find;return typeof f==="function"?await f.call(baseModel,id)!=null:!1};if(typeof baseModel.firstOrCreate!=="function")baseModel.firstOrCreate=async function(attrs,defaults={}){if(!attrs||typeof attrs!=="object"||Array.isArray(attrs))throw Error(`[ORM] ${definition.name}.firstOrCreate requires a where-attrs object`);const q=getWhere("firstOrCreate").call(baseModel,attrs),existing=typeof q.first==="function"?await q.first():await q;if(existing)return existing;const payload={...attrs,...defaults};applyMassAssignmentRules(definition,payload);return await getCreate("firstOrCreate").call(baseModel,payload)};if(typeof baseModel.updateOrCreate!=="function")baseModel.updateOrCreate=async function(attrs,values){if(!attrs||typeof attrs!=="object"||Array.isArray(attrs))throw Error(`[ORM] ${definition.name}.updateOrCreate requires a where-attrs object`);if(!values||typeof values!=="object"||Array.isArray(values))throw Error(`[ORM] ${definition.name}.updateOrCreate requires a values object`);const q=getWhere("updateOrCreate").call(baseModel,attrs),existing=typeof q.first==="function"?await q.first():await q;if(existing){applyMassAssignmentRules(definition,values);const id=existing[pk];await getWhere("updateOrCreate").call(baseModel,pk,id).update(values);const f=baseModel.find;return typeof f==="function"?await f.call(baseModel,id):{...existing,...values}}const payload={...attrs,...values};applyMassAssignmentRules(definition,payload);return await getCreate("updateOrCreate").call(baseModel,payload)};if(typeof baseModel.count!=="function")baseModel.count=async function(){const q=baseModel;if(typeof q.query==="function"){const builder=q.query();if(typeof builder?.count==="function"){const n=await builder.count();return Number(n)||0}}const{all,get}=baseModel;if(typeof all==="function")return(await all.call(baseModel)||[]).length;if(typeof get==="function")return(await get.call(baseModel)||[]).length;return 0};if(typeof baseModel.pluck!=="function")baseModel.pluck=async function(column){if(!column||typeof column!=="string")throw Error(`[ORM] ${definition.name}.pluck requires a column name`);const all=baseModel.all;return((typeof all==="function"?await all.call(baseModel):[])||[]).map((r)=>r?.[column])};if(typeof baseModel.whereIn!=="function")baseModel.whereIn=async function(column,values){if(!column||typeof column!=="string")throw Error(`[ORM] ${definition.name}.whereIn requires a column name`);if(!Array.isArray(values))throw Error(`[ORM] ${definition.name}.whereIn requires an array of values`);if(values.length===0)return[];const w=baseModel.where;if(typeof w!=="function")return[];const q=w.call(baseModel,column,"in",values);if(q&&typeof q.get==="function")return await q.get();return await q};const orderHelpers=[["latest","desc"],["oldest","asc"]];for(const[name,dir]of orderHelpers)if(typeof baseModel[name]!=="function")baseModel[name]=async function(column="created_at"){const q=baseModel,builder=typeof q.query==="function"?q.query():q;if(typeof builder?.orderBy==="function"){const ordered=builder.orderBy(column,dir);if(typeof ordered.first==="function")return await ordered.first();if(typeof ordered.get==="function")return(await ordered.get())?.[0]??null}const all=baseModel.all;if(typeof all!=="function")return null;return[...await all.call(baseModel)||[]].sort((a,b)=>{const av=a?.[column],bv=b?.[column];if(av===bv)return 0;return(av>bv?1:-1)*(dir==="desc"?-1:1)})[0]??null};const searchConfig=resolveSearchConfig(definition.traits?.useSearch,definition);if(searchConfig){const indexName=searchConfig.index??definition.table??snakeCase(`${definition.name}s`);if(typeof baseModel.search!=="function")baseModel.search=function(query,params){return createSearchQueryBuilder(baseModel,indexName,query,params,searchConfig.searchable)};if(typeof baseModel.makeAllSearchable!=="function")baseModel.makeAllSearchable=async function(chunkSize=500){const all=baseModel.all;if(typeof all!=="function")throw Error(`[ORM] ${definition.name}.makeAllSearchable cannot run: the underlying query builder did not expose all()`);const{useSearchEngine}=await import("@stacksjs/search-engine"),engine=useSearchEngine();let total=0;const query=baseModel.query;if(typeof query==="function")try{const q=query.call(baseModel);if(q&&typeof q.chunk==="function"){await q.chunk(chunkSize,async(rows)=>{const docs=await projectDocumentsFromTrait(rows,searchConfig);if(docs.length>0)await engine.addDocuments(indexName,docs);total+=docs.length});return total}}catch{}const rows=await all.call(baseModel)||[],docs=await projectDocumentsFromTrait(rows,searchConfig);if(docs.length>0)await engine.addDocuments(indexName,docs);return docs.length};if(typeof baseModel.removeAllFromSearch!=="function")baseModel.removeAllFromSearch=async function(){const{searchEngineReady}=await import("@stacksjs/search-engine");await(await searchEngineReady()).deleteAllDocuments(indexName)}}}function searchProjectionRecord(document){if(!document||typeof document!=="object"||Array.isArray(document))throw TypeError("[orm/search] A search projection must return a document object");return Object.fromEntries(Object.entries(document))}export async function projectDocumentsFromTrait(models,config){if(models.length===0)return[];if(typeof config.shapeMany==="function"){const docs=await config.shapeMany(models);if(!Array.isArray(docs)||docs.length!==models.length)throw TypeError(`[orm/search] shapeMany returned ${Array.isArray(docs)?docs.length:typeof docs} documents for ${models.length} rows. It must return one document per row, in order, or documents are indexed under the wrong ids.`);return docs.filter(Boolean).map(searchProjectionRecord)}return(await Promise.all(models.map((model)=>projectDocumentFromTrait(model,config)))).filter(Boolean)}function hiddenAttributeNames(definition){const hidden=new Set,attributes=definition.attributes??{};for(const[name,attribute]of Object.entries(attributes))if(attribute?.hidden)hidden.add(name);return hidden}function withoutHidden(document,hidden){if(!document||hidden.size===0)return document;const safe={};for(const[key,value]of Object.entries(document))if(!hidden.has(key))safe[key]=value;return safe}async function projectDocumentFromTrait(model,config){if(typeof config.shape==="function"){const document=await config.shape(model);return document==null?document:searchProjectionRecord(document)}const fromHook=model?.toSearchableObject?.();if(fromHook!==void 0)return fromHook;return withoutHidden(model?._attributes??(typeof model?.toJSON==="function"?model.toJSON():null),config.hidden??new Set)}function createSearchQueryBuilder(_baseModel,indexName,query,initialParams,searchableFields){const defaultQueryBy=searchableFields&&searchableFields.length>0?{query_by:[...searchableFields].join(",")}:{};let params={q:query,...defaultQueryBy,...initialParams??{}};const builder={with(extra){params={...params,...extra};return builder},where(filter){params={...params,filter};return builder},async get(){const{useSearchEngine}=await import("@stacksjs/search-engine");return(await useSearchEngine().search(indexName,params))?.hits??[]},async paginate(perPage,page=1){if(!Number.isInteger(perPage)||perPage<1)throw TypeError(`[orm/search] paginate() needs a positive integer page size, got ${perPage}`);if(!Number.isInteger(page)||page<1)throw TypeError(`[orm/search] paginate() pages are 1-based, got ${page}`);const{useSearchEngine}=await import("@stacksjs/search-engine"),result=await useSearchEngine().search(indexName,{...params,limit:perPage,offset:(page-1)*perPage});return{hits:result.hits??[],total:Number(result.estimatedTotalHits??result.nbHits??result.total??0),page,perPage}},async raw(){const{useSearchEngine}=await import("@stacksjs/search-engine");return await useSearchEngine().search(indexName,params)}};return builder}function encryptedAliases(key){const snake=snakeCase(key);return snake===key?[key]:[key,snake]}async function decryptAttrsInPlace(row,encryptedKeys){if(!row||typeof row!=="object")return;const bag=row._attributes??row;for(const key of encryptedKeys)for(const alias of encryptedAliases(key)){if(!(alias in bag))continue;const value=bag[alias];if(!isEncrypted(value))continue;bag[alias]=await decryptValue(value)}}function wrapReadsWithEncryption(baseModel,encryptedKeys){if(encryptedKeys.length===0)return;const decryptResult=async(r)=>{if(Array.isArray(r)){for(const item of r)await decryptAttrsInPlace(item,encryptedKeys);return r}if(r&&typeof r==="object"&&Array.isArray(r.data)){for(const item of r.data)await decryptAttrsInPlace(item,encryptedKeys);return r}if(r)await decryptAttrsInPlace(r,encryptedKeys);return r},directReads=["find","first","last","all","firstOrFail","findOrFail","findMany"];for(const method of directReads){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=async function(...args){const result=await original.apply(this,args);return await decryptResult(result)}}const writeReturningInstance=["create","createMany","firstOrCreate","updateOrCreate","make"];for(const method of writeReturningInstance){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=async function(...args){const result=await original.apply(this,args);return await decryptResult(result)}}for(const method of["update","forceUpdate"]){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=async function(id,data){const result=await original.call(this,id,data);return await decryptResult(result)}}const terminals=["first","firstOrFail","get","all","find","findOrFail","last","paginate","simplePaginate"],decorateBuilder=(builder)=>{if(!builder||typeof builder!=="object")return builder;const target=builder;if(target.__stacksEncryptedReads)return builder;for(const method of terminals){const original=target[method];if(typeof original!=="function")continue;target[method]=async function(...args){return await decryptResult(await original.apply(this,args))}}for(const method of["where","whereIn","whereNull","whereNotNull","orWhere","orderBy","orderByDesc","limit","offset","with"]){const original=target[method];if(typeof original!=="function")continue;target[method]=function(...args){return decorateBuilder(original.apply(this,args))}}Object.defineProperty(target,"__stacksEncryptedReads",{value:!0,enumerable:!1});return builder};for(const entry of["where","whereIn","whereNull","whereNotNull","orderBy","orderByDesc","limit","query"]){const original=baseModel[entry];if(typeof original!=="function")continue;baseModel[entry]=function(...args){return decorateBuilder(original.apply(this,args))}}}function wrapWritesWithEncryption(baseModel,encryptedKeys){if(encryptedKeys.length===0)return;const encryptArg=async(data)=>{if(!data||typeof data!=="object"||Array.isArray(data))return data;const out={...data};for(const key of encryptedKeys)for(const alias of encryptedAliases(key))if(alias in out)out[alias]=await encryptValue(out[alias]);return out},writeMethods=["create","firstOrCreate","updateOrCreate","forceCreate"],originalCreateMany=baseModel.createMany;if(typeof originalCreateMany==="function")baseModel.createMany=async function(items,...rest){const encrypted=Array.isArray(items)?await Promise.all(items.map(encryptArg)):items;return await originalCreateMany.call(this,encrypted,...rest)};for(const method of writeMethods){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=async function(...args){args[0]=await encryptArg(args[0]);if(args[1])args[1]=await encryptArg(args[1]);return await original.apply(this,args)}}for(const method of["update","forceUpdate"]){const original=baseModel[method];if(typeof original!=="function")continue;baseModel[method]=async function(id,data){const enc=await encryptArg(data);return await original.call(this,id,enc)}}}function wrapWritesWithMassAssignment(baseModel,definition){const origCreate=baseModel.create;if(typeof origCreate==="function")baseModel.create=async function(data,...rest){applyMassAssignmentRules(definition,data);const finalData=await applyDefinedSetters(definition,data);return await origCreate.call(this,finalData,...rest)};const origCreateMany=baseModel.createMany;if(typeof origCreateMany==="function")baseModel.createMany=async function(items,...rest){const finalItems=[];for(const data of items){applyMassAssignmentRules(definition,data);finalItems.push(await applyDefinedSetters(definition,data))}return await origCreateMany.call(this,finalItems,...rest)};for(const method of["firstOrCreate","updateOrCreate"]){const orig=baseModel[method];if(typeof orig!=="function")continue}}function wrapWritesWithValidation(baseModel,definition){if(!hasWriteValidators(definition))return;const modelName=String(definition?.name??baseModel.name??"Model"),check=(data,hook)=>{if(validationIsSuppressed())return;if(!data||typeof data!=="object"||Array.isArray(data))return;const result=validateWriteBody(data,definition,hook);if(!result.valid)throw new ModelValidationError(modelName,result.errors)},origCreate=baseModel.create;if(typeof origCreate==="function")baseModel.create=async function(data,...rest){check(data,"creating");return await origCreate.call(this,data,...rest)};const origCreateMany=baseModel.createMany;if(typeof origCreateMany==="function")baseModel.createMany=async function(items,...rest){if(Array.isArray(items))for(const data of items)check(data,"creating");return await origCreateMany.call(this,items,...rest)};const origUpdate=baseModel.update;if(typeof origUpdate==="function")baseModel.update=async function(id,data,...rest){check(data,"updating");return await origUpdate.call(this,id,data,...rest)}}function wrapQueryMethodsWithCasts(baseModel,casts){const writeMethods=["create","firstOrCreate","updateOrCreate"],originalCreateMany=baseModel.createMany;if(typeof originalCreateMany==="function")baseModel.createMany=async function(items,...rest){const cast=Array.isArray(items)?items.map((item)=>item&&typeof item==="object"&&!Array.isArray(item)?castAttributes(item,casts,"set"):item):items;return await originalCreateMany.call(this,cast,...rest)};for(const method of writeMethods){const original=baseModel[method];if(typeof original==="function")baseModel[method]=async function(...args){log.debug(`[orm] ${method.charAt(0).toUpperCase()+method.slice(1)} ${String(baseModel.name||"unknown")}`,args[0]);if(args[0]&&typeof args[0]==="object"&&!Array.isArray(args[0]))args[0]=castAttributes(args[0],casts,"set");return await original.apply(this,args)}}const origUpdate=baseModel.update;if(typeof origUpdate==="function")baseModel.update=async function(id,data){const cast=data&&typeof data==="object"&&!Array.isArray(data)?castAttributes(data,casts,"set"):data;return await origUpdate.call(this,id,cast)}}import{createMessagingMethods}from"./messaging";import{createTaggableMethods}from"./traits/taggable";import{createCategorizableMethods}from"./traits/categorizable";import{createCommentableMethods}from"./traits/commentable";import{BILLABLE_INSTANCE_METHODS,createBillableMethods}from"./traits/billable";import{createLikeableMethods}from"./traits/likeable";import{createTwoFactorMethods}from"./traits/two-factor";import{createSoftDeleteMethods,resolveSoftDeleteOptions,cascadeSoftDelete}from"./traits/soft-deletes";import{applyAudit,resolveAuditOptions}from"./traits/audit";import{applyActivityLog,resolveActivityLogOptions}from"./traits/activity-log";import{collectEncryptedAttributes,decryptValue,encryptValue,isEncrypted}from"./utils/encrypted";const KNOWN_TRAITS=new Set(["useUuid","useTimestamps","timestampable","useSoftDeletes","softDeletable","useApi","useAuth","authenticatable","useSearch","searchable","useSeeder","seedable","useSocials","billable","observe","useAudit","useActivityLog","prunable","sharding","likeable","taggable","categorizable","useMessaging","commentable","commentables","broadcastOn","broadcastWith","gdpr"]);export function defineModel(definition){log.debug(`[orm] Defining model: ${definition.name} (table: ${definition.table})`);for(const trait of Object.keys(definition.traits??{}))if(!KNOWN_TRAITS.has(trait))log.warn(`[orm] Model '${definition.name}' declares an unknown trait '${trait}'. It does nothing. Known traits: ${[...KNOWN_TRAITS].sort().join(", ")}.`);const observeHooks=buildEventHooks(definition),searchHooks=buildSearchHooks(definition),hooks=mergeModelHooks(observeHooks,searchHooks),traitMethods=buildTraitMethods(definition);definition.__traitMethods=traitMethods;const authenticatable=definition.traits;if((authenticatable?.useAuth||authenticatable?.authenticatable)&&definition.name!=="PersonalAccessToken"){const declared=definition.morphMany;if(!declared||!("tokenable"in declared))definition.morphMany={...declared??{},tokenable:"PersonalAccessToken"}}const defWithHooks=hooks?{...definition,hooks:{...definition.hooks,...hooks}}:definition,baseModel=createModel(defWithHooks);wrapReadsWithProxy(baseModel,definition.casts);addStaticHelpers(baseModel,defWithHooks);wrapWritesWithMassAssignment(baseModel,defWithHooks);if(definition.casts&&Object.keys(definition.casts).length>0)wrapQueryMethodsWithCasts(baseModel,definition.casts);const encryptedKeys=collectEncryptedAttributes(definition);if(encryptedKeys.length>0){wrapWritesWithEncryption(baseModel,encryptedKeys);wrapReadsWithEncryption(baseModel,encryptedKeys)}const softDeleteFlag=definition.traits?.useSoftDeletes;if(softDeleteFlag)applySoftDeletes(baseModel,defWithHooks,softDeleteFlag);const useAuditDecl=definition.traits?.useAudit;if(useAuditDecl){const auditOpts=resolveAuditOptions(useAuditDecl);applyAudit(baseModel,definition.name,definition.primaryKey||"id",auditOpts)}const activityOptions=resolveActivityLogOptions(definition.traits?.useActivityLog);if(activityOptions)applyActivityLog(baseModel,definition.name,definition.primaryKey||"id",activityOptions,[...hiddenAttributeNames(definition)]);wrapWritesWithValidation(baseModel,defWithHooks);const withHelpers=baseModel;withHelpers.withoutValidation=function(fn){return withoutValidation(fn)};withHelpers.withoutEvents=function(fn){return withoutEvents(fn)};for(const method of["create","createMany","update","firstOrCreate","updateOrCreate","forceCreate","forceUpdate","delete","forceDelete","softDelete","restore"]){const orig=baseModel[method];if(typeof orig!=="function")continue;const quietName=`${method}Quietly`;if(typeof baseModel[quietName]==="function")continue;baseModel[quietName]=function(...args){return withoutEvents(()=>orig.apply(baseModel,args))}}const finalModel=Object.assign(baseModel,traitMethods,{...definition,getDefinition:()=>definition,_isStacksModel:!0});registerModel(definition.name,finalModel);Object.defineProperty(finalModel,MODEL_DEFINITION,{value:definition,enumerable:!1,configurable:!0});return finalModel}function buildEventHooks(definition){const observe=definition.traits?.observe;if(!observe)return;const modelName=definition.name.toLowerCase(),toEventPayload=(model)=>{const attributes=model?.attributes;if(!attributes||typeof attributes!=="object")return model;const pk=definition.primaryKey||"id",payload={...attributes};if(payload[pk]===void 0&&model[pk]!==void 0)payload[pk]=model[pk];return payload},dispatchEvent=async(event,data)=>{if(eventsAreSuppressed())return;try{const{dispatch}=modelEventsModule??=await import("@stacksjs/events");await dispatch(event,data)}catch(err){if(err&&typeof err==="object"&&"code"in err&&err.code==="MODULE_NOT_FOUND")return;console.error(`[ORM] Event '${event}' handler error:`,err);if(process.env.STACKS_ORM_EVENT_ERRORS!=="swallow")throw err}},broadcastOnFn=definition.traits?.broadcastOn,broadcastWithFn=definition.traits?.broadcastWith,broadcastingEnabled=typeof broadcastOnFn==="function"&&typeof broadcastWithFn==="function",dispatchBroadcast=async(event,model)=>{if(!broadcastingEnabled)return;if(eventsAreSuppressed())return;try{const channelsRaw=broadcastOnFn(model),channels=Array.isArray(channelsRaw)?channelsRaw:[channelsRaw],payload=broadcastWithFn(model),broadcast=(await import("@stacksjs/realtime").catch(()=>null))?.broadcast;if(typeof broadcast!=="function")return;for(const channel of channels){if(typeof channel!=="string"||channel.length===0)continue;await broadcast(channel,event,payload)}}catch(err){if(err&&typeof err==="object"&&"code"in err&&err.code==="MODULE_NOT_FOUND")return;console.error(`[ORM] Broadcast '${event}' handler error:`,err);if(process.env.STACKS_ORM_BROADCAST_ERRORS==="throw")throw err}},dispatchBeforeEvent=async(event,data)=>{if(eventsAreSuppressed())return!0;let eventsModule;try{eventsModule=modelEventsModule??=await import("@stacksjs/events");const{dispatchAndCollect}=eventsModule,results=await dispatchAndCollect(event,data);if(results.some((result)=>result.ok&&result.value===!1))return!1;const failure=results.find((result)=>!result.ok);if(failure&&!failure.ok)throw failure.error}catch(err){if(!eventsModule&&err&&typeof err==="object"&&"code"in err&&err.code==="MODULE_NOT_FOUND")return!0;console.error(`[ORM] Before-event '${event}' handler error:`,err);if(process.env.STACKS_ORM_EVENT_ERRORS!=="swallow")throw err}return!0},events=observe===!0?["create","update","delete"]:Array.isArray(observe)?observe:[],hooks={};if(events.includes("create")){hooks.beforeCreate=async(model)=>{log.debug(`[orm] Create ${modelName}`,model);if(!await dispatchBeforeEvent(`${modelName}:saving`,model))throw Error(`[ORM] ${modelName}:saving event cancelled the operation`);if(!await dispatchBeforeEvent(`${modelName}:creating`,model))throw Error(`[ORM] ${modelName}:creating event cancelled the operation`)};hooks.afterCreate=async(model)=>{const payload=toEventPayload(model);await dispatchEvent(`${modelName}:created`,payload);await dispatchEvent(`${modelName}:saved`,payload);await dispatchBroadcast(`${modelName}:created`,model)}}if(events.includes("update")){hooks.beforeUpdate=async(model)=>{log.debug(`[orm] Update ${modelName}#${model?.id??"unknown"}`,model);if(!await dispatchBeforeEvent(`${modelName}:saving`,model))throw Error(`[ORM] ${modelName}:saving event cancelled the operation`);if(!await dispatchBeforeEvent(`${modelName}:updating`,model))throw Error(`[ORM] ${modelName}:updating event cancelled the operation`)};hooks.afterUpdate=async(model)=>{const payload=toEventPayload(model);await dispatchEvent(`${modelName}:updated`,payload);await dispatchEvent(`${modelName}:saved`,payload);await dispatchBroadcast(`${modelName}:updated`,model)}}if(events.includes("delete")){hooks.beforeDelete=async(model)=>{log.debug(`[orm] Delete ${modelName}#${model?.id??"unknown"}`);if(!await dispatchBeforeEvent(`${modelName}:deleting`,model))throw Error(`[ORM] ${modelName}:deleting event cancelled the operation`)};hooks.afterDelete=async(model)=>{await dispatchEvent(`${modelName}:deleted`,toEventPayload(model));await dispatchBroadcast(`${modelName}:deleted`,model)}}return hooks}function mergeModelHooks(...sets){const merged={},names=new Set;for(const set of sets){if(!set)continue;for(const name of Object.keys(set))names.add(name)}for(const name of names){const fns=sets.map((s)=>s?.[name]).filter((fn)=>typeof fn==="function");if(!fns.length)continue;merged[name]=async(...args)=>{for(const fn of fns)await fn(...args)}}return Object.keys(merged).length?merged:void 0}function resolveSearchConfig(useSearch,definition){if(!useSearch)return null;const base=useSearch===!0?{}:typeof useSearch==="object"?{...useSearch}:null;if(!base)return null;if(definition)base.hidden=hiddenAttributeNames(definition);return base}function buildSearchHooks(definition){const config=resolveSearchConfig(definition.traits?.useSearch,definition);if(!config)return;const searchConfig=config,indexName=searchConfig.index??definition.table??snakeCase(`${definition.name}s`);async function projectDocument(model){if(typeof searchConfig.shapeMany==="function")try{const[doc]=await projectDocumentsFromTrait([model],searchConfig);return doc}catch(err){log.warn(`[orm/search] shapeMany() threw for ${definition.name}: ${err.message}`);return}if(typeof searchConfig.shape==="function")try{return await projectDocumentFromTrait(model,searchConfig)}catch(err){log.warn(`[orm/search] shape() threw for ${definition.name}: ${err.message}`);return}const fromHook=wrapModelInstance(model).toSearchableObject?.();if(fromHook!==void 0)return fromHook;return model?._attributes??(typeof model?.toJSON==="function"?model.toJSON():null)}const syncDocument=async(model)=>{if(eventsAreSuppressed())return;const doc=await projectDocument(model);if(!doc)return;if(config.queueable){try{const{Jobs}=await import("@stacksjs/queue");await Jobs.dispatch("SyncSearchIndexJob",{index:indexName,op:"upsert",doc})}catch(err){log.warn(`[orm/search] queue dispatch failed for ${definition.name}; falling back to inline: ${err.message}`);await indexInline(indexName,doc,definition.name)}return}await indexInline(indexName,doc,definition.name)};return{afterCreate:syncDocument,afterUpdate:syncDocument,afterDelete:async(model)=>{if(eventsAreSuppressed())return;const id=model?.id??model?._attributes?.id;if(id==null)return;if(config.queueable){try{const{Jobs}=await import("@stacksjs/queue");await Jobs.dispatch("SyncSearchIndexJob",{index:indexName,op:"delete",id:Number(id)})}catch(err){log.warn(`[orm/search] queue dispatch failed for ${definition.name}#${id}; falling back to inline: ${err.message}`);await removeInline(indexName,Number(id),definition.name)}return}await removeInline(indexName,Number(id),definition.name)}}}const searchFailures=new Map;function warnSearchFailure(key,message){const seen=searchFailures.get(key)??0;searchFailures.set(key,seen+1);if(seen===0)log.warn(`[orm/search] ${message}`);else if(seen===9)log.warn(`[orm/search] ${key} is still failing; further identical warnings suppressed for this process.`)}async function indexInline(indexName,doc,modelName){try{const{useSearchEngine}=await import("@stacksjs/search-engine");await useSearchEngine().addDocument(indexName,doc)}catch(err){warnSearchFailure(`index:${modelName}`,`Failed to index ${modelName}: ${err.message}`)}}async function removeInline(indexName,id,modelName){try{const{useSearchEngine}=await import("@stacksjs/search-engine");await useSearchEngine().deleteDocument(indexName,id)}catch(err){warnSearchFailure(`remove:${modelName}`,`Failed to remove ${modelName}#${id}: ${err.message}`)}}function buildTraitMethods(definition){const methods={},{table:tableName,traits}=definition;if(!traits)return methods;if(traits?.useMessaging)methods._messaging=createMessagingMethods(tableName);if(traits.taggable)methods._taggable=createTaggableMethods(tableName);if(traits.categorizable)methods._categorizable=createCategorizableMethods(tableName);if(traits.commentable)methods._commentable=createCommentableMethods(tableName);if(traits.billable)methods._billable=createBillableMethods(tableName);if(traits.likeable){const likeableOpts=typeof traits.likeable==="object"?traits.likeable:void 0;methods._likeable=createLikeableMethods(tableName,likeableOpts)}const useAuth=traits.useAuth||traits.authenticatable;if(typeof useAuth==="object"&&useAuth&&"useTwoFactor"in useAuth&&useAuth.useTwoFactor)methods._twoFactor=createTwoFactorMethods();return methods}function applySoftDeletes(baseModel,definition,traitFlag){const helpers=createSoftDeleteMethods(baseModel,definition.primaryKey||"id"),options=resolveSoftDeleteOptions(traitFlag),parentDef=definition,modelName=definition.name.toLowerCase(),observeOn=definition.traits?.observe!=null&&definition.traits?.observe!==!1,fireRestoring=async(id)=>{if(!observeOn||eventsAreSuppressed())return!0;try{const{dispatchAsync}=modelEventsModule??=await import("@stacksjs/events"),results=await dispatchAsync(`${modelName}:restoring`,{id});if(Array.isArray(results)&&results.some((r)=>r===!1))return!1}catch(err){if(err?.code!=="MODULE_NOT_FOUND")console.error(`[ORM] Before-event '${modelName}:restoring' handler error:`,err)}return!0},fireRestored=async(id)=>{if(!observeOn||eventsAreSuppressed())return;try{const{dispatch}=modelEventsModule??=await import("@stacksjs/events");await dispatch(`${modelName}:restored`,{id})}catch(err){if(err?.code!=="MODULE_NOT_FOUND")console.error(`[ORM] Event '${modelName}:restored' handler error:`,err)}},transactional=process.env.STACKS_ORM_CASCADE_SWALLOW!=="true",softDeleteFn=async(id)=>{if(!options.cascade?.length||!transactional){const ok=await helpers.softDelete(id);if(ok&&options.cascade?.length)await cascadeSoftDelete(parentDef,options,id,"softDelete");return ok}const{db}=await import("@stacksjs/database/runtime");return await db.transaction(async()=>{const ok=await helpers.softDelete(id);if(ok)await cascadeSoftDelete(parentDef,options,id,"softDelete");return ok})},restoreFn=async(id)=>{if(!await fireRestoring(id))return!1;if(!options.cascade?.length||!transactional){const ok=await helpers.restore(id);if(ok){if(options.cascade?.length)await cascadeSoftDelete(parentDef,options,id,"restore");await fireRestored(id)}return ok}const{db}=await import("@stacksjs/database/runtime"),ok=await db.transaction(async()=>{const inner=await helpers.restore(id);if(inner)await cascadeSoftDelete(parentDef,options,id,"restore");return inner});if(ok)await fireRestored(id);return ok};baseModel.softDelete=softDeleteFn;baseModel.restore=restoreFn;baseModel.forceDelete=helpers.forceDelete;baseModel.withTrashed=helpers.withTrashed;baseModel.onlyTrashed=helpers.onlyTrashed;baseModel.delete=softDeleteFn}export function toAttrs(value){if(value==null)return value;if(Array.isArray(value))return value.map((v)=>toAttrs(v));if(typeof value!=="object")return value;const candidate=value;if(typeof candidate.toJSON==="function")return candidate.toJSON();if(candidate._attributes&&typeof candidate._attributes==="object")return candidate._attributes;return value}
package/dist/routes.js CHANGED
@@ -1 +1 @@
1
- import process from"node:process";import{parseBearerToken,route}from"@stacksjs/router";import{projectPath,storagePath}from"@stacksjs/path";import{createQueryBuilder,config as queryBuilderConfig,setConfig}from"@stacksjs/query-builder";import{HttpError}from"@stacksjs/error-handling";import{log}from"@stacksjs/logging/runtime";import{API_ABILITIES,apiBasePath,applyCasts,applySorting,buildIndexPaginator,buildReadColumnMap,dashboardRoleMiddleware,describeContradictoryRoleGates,describeUnscopedMutatingModels,dropHiddenInputs,filterFillable,getWritableFields,indexRouteShapes,mapWriteError,ownershipDeclaredUnscoped,policyDecision,resolveAbilityMiddleware,resolveIndexPageArgs,resolveRowScopingPolicy,routeShapeKey,stampOwnership,stripHidden,toSnakeCase,toSnakeCaseKeys,validateWriteBody}from"./auto-crud";import{loadModelRegistryWithOrigins,resolveModelApiSelection,selectApiModels}from"./model-registry";import{effectiveOwnershipConfig}from"./ownership";import{connectionConflictWarning,splitProjectQueryBuilderConfig}from"./project-query-builder-config";const qbConfigPath=projectPath("config/qb.ts");let projectQbConfig;try{projectQbConfig=(await import(qbConfigPath)).default}catch{log.debug("[orm] No config/qb.ts override found - using the connection from config/database.ts")}if(projectQbConfig){const{behaviour,conflicts}=splitProjectQueryBuilderConfig(projectQbConfig,queryBuilderConfig);if(conflicts.length)log.warn(connectionConflictWarning(conflicts));setConfig(behaviour)}const registry=await loadModelRegistryWithOrigins({defaultsRoot:storagePath("framework/defaults/app/Models"),userRoot:projectPath("app/Models"),onImportError:(file,error)=>log.warn(`[orm] Could not load model ${file}: ${error.message}`)}),models=registry.models;function dynamicDb(){return createQueryBuilder()}const routeShapeIndex=indexRouteShapes(route.routes);function routeClaimed(method,path){const key=routeShapeKey(method,path),existing=routeShapeIndex.get(key);if(!existing){routeShapeIndex.set(key,{method,path});return!1}const existingPath=String(existing.path??"");log.debug(`[orm] Skipping generated ${method} ${path} - already defined${existingPath&&existingPath!==path?` as ${existingPath}`:""}.`);return!0}function getHiddenFields(model){if(!model.attributes)return[];return Object.entries(model.attributes).filter(([_,attr])=>attr.hidden===!0).map(([name])=>name)}function pluralize(s){if(s.endsWith("y")&&!/[aeiou]y$/i.test(s))return`${s.slice(0,-1)}ies`;if(/(?:s|x|z|ch|sh)$/i.test(s))return`${s}es`;return`${s}s`}async function applyIncludes(rows,model,includeParam,modelsRegistry){if(!rows.length)return rows;const requested=includeParam.split(",").map((s)=>s.trim()).filter(Boolean);if(!requested.length)return rows;const belongsTo=Array.isArray(model.belongsTo)?model.belongsTo:[],hasMany=Array.isArray(model.hasMany)?model.hasMany:[],hasOne=Array.isArray(model.hasOne)?model.hasOne:[],allowed=new Map;for(const m of belongsTo)allowed.set(toSnakeCase(m),{kind:"belongsTo",modelName:m});for(const m of hasOne)allowed.set(toSnakeCase(m),{kind:"hasOne",modelName:m});for(const m of hasMany)allowed.set(pluralize(toSnakeCase(m)),{kind:"hasMany",modelName:m});for(const include of requested){const meta=allowed.get(include);if(!meta)continue;const related=modelsRegistry[meta.modelName];if(!related)continue;const relatedTable=related.table||pluralize(toSnakeCase(meta.modelName)),relatedHidden=getHiddenFields(related),relatedAttrs=(id)=>stripHidden(applyReadCasts(id,related),relatedHidden);if(meta.kind==="belongsTo"||meta.kind==="hasOne"){const fkOnParent=`${toSnakeCase(meta.modelName)}_id`,ids=[...new Set(rows.map((r)=>r[fkOnParent]).filter((v)=>v!=null))];if(!ids.length){for(const r of rows)r[include]=null;continue}const childRows=await dynamicDb().selectFrom(relatedTable).whereIn("id",ids).get(),byId=new Map;for(const cr of childRows??[])byId.set(String(cr.id),relatedAttrs(cr));for(const r of rows)r[include]=byId.get(String(r[fkOnParent]))??null}else{const parentName=String(model.name??""),fkOnChild=`${toSnakeCase(parentName)}_id`,parentIds=[...new Set(rows.map((r)=>r.id).filter((v)=>v!=null))];if(!parentIds.length){for(const r of rows)r[include]=[];continue}const childRows=await dynamicDb().selectFrom(relatedTable).whereIn(fkOnChild,parentIds).get(),grouped=new Map;for(const cr of childRows??[]){const key=String(cr[fkOnChild]),arr=grouped.get(key)??[];arr.push(relatedAttrs(cr));grouped.set(key,arr)}for(const r of rows)r[include]=grouped.get(String(r.id))??[]}}return rows}async function applyDefinedSetters(data,model){const setters=model?.set;if(!setters||typeof setters!=="object")return data;const out={...data};for(const[key,fn]of Object.entries(setters)){if(typeof fn!=="function"||!(key in out))continue;try{out[key]=await fn(out)}catch{}}return out}function applySetCasts(data,model){return applyCasts(data,model?.casts,"set")}function applyReadCasts(row,model){return applyCasts(row,model?.casts,"get")}function jsonResponse(data,status=200){return new Response(JSON.stringify(data),{status,headers:{"Content-Type":"application/json"}})}function errorResponse(req,body,status=400){const reqId=req?._requestId,enriched=reqId?{...body,request_id:reqId}:body;return new Response(JSON.stringify(enriched),{status,headers:{"Content-Type":"application/json"}})}const MAX_BODY_BYTES=1048576;async function getRequestBody(req){if(Number(req.headers?.get?.("content-length")??0)>MAX_BODY_BYTES)throw new HttpError(413,`Request body exceeds ${MAX_BODY_BYTES}-byte limit`);if(req.jsonBody&&typeof req.jsonBody==="object")return req.jsonBody;if(req.formBody&&typeof req.formBody==="object")return req.formBody;try{const text=await req.clone().text();if(text.length>MAX_BODY_BYTES)throw new HttpError(413,`Request body exceeds ${MAX_BODY_BYTES}-byte limit`);if(!text)return{};const parsed=JSON.parse(text);return parsed&&typeof parsed==="object"&&!Array.isArray(parsed)?parsed:{}}catch(err){if(err instanceof HttpError)throw err;throw new HttpError(400,`Invalid JSON body: ${err.message}`)}}function coerceId(raw){if(raw==null)return null;if(typeof raw==="number")return Number.isFinite(raw)&&raw>0?raw:null;if(typeof raw==="string"){const trimmed=raw.trim();if(!trimmed)return null;if(/^\d+$/.test(trimmed)){const n=Number(trimmed);return Number.isFinite(n)&&n>0?n:null}return trimmed}return null}function bearerOf(req){const fn=req.bearerToken;if(typeof fn==="function"){const t=fn.call(req);if(t)return t}return parseBearerToken(req.headers?.get?.("authorization")??null)}async function authedUserFromRequest(req){const stored=req._authenticatedUser;if(stored)return stored;const token=bearerOf(req);if(!token)return null;try{const{Auth}=await import("@stacksjs/auth"),user=await Auth.getUserFromToken(token);if(user)req._authenticatedUser=user;return user||null}catch{return null}}let authGateModule;async function authGate(){try{const{Gate}=authGateModule??=await import("@stacksjs/auth");return Gate}catch{return null}}async function hasPolicyFor(modelName){const gate=await authGate();return!!gate&&gate.hasPolicy(modelName)}async function policyRefusal(modelName,ability,req,subject){const refusal=await policyDecision(modelName,ability,{gate:authGate,user:()=>authedUserFromRequest(req)},subject);return refusal?jsonResponse({error:refusal.message},refusal.status):null}function ownsRow(rowField,ownerValue){if(rowField==null||ownerValue==null)return!1;if(Array.isArray(ownerValue))return ownerValue.some((v)=>String(v)===String(rowField));return String(rowField)===String(ownerValue)}async function resolveOwnership(model,user,req){const cfg=effectiveOwnershipConfig(model);if(!cfg||!cfg.field||typeof cfg.resolve!=="function")return{enforced:!1,value:null,field:"",bypass:!1};const bypass=typeof cfg.bypass==="function"?!!cfg.bypass(user,req):!1;let value=null;try{value=await cfg.resolve(user,req)}catch{value=null}return{enforced:!0,value,field:String(cfg.field),bypass}}async function resolveAuthedFill(model,hook,user,req){const cfg=model?.authedFill?.[hook];if(!cfg)return{};if(typeof cfg==="function")try{const result=await cfg(user,req);return result&&typeof result==="object"?result:{}}catch{return{}}if(typeof cfg==="object"){const out={};for(const[field,getter]of Object.entries(cfg))try{out[field]=typeof getter==="function"?await getter(user,req):getter}catch{}return out}return{}}const securityApiConfig=await(async()=>{try{return(await import(projectPath("config/security.ts"))).default?.api}catch{return}})(),rowScopingPolicy=resolveRowScopingPolicy(securityApiConfig?.rowScoping,process.env.STACKS_API_ROW_SCOPING),apiModels=(()=>{const selection=resolveModelApiSelection(securityApiConfig?.models,process.env.STACKS_MODEL_APIS),{models:selected,unmatched}=selectApiModels(registry,selection);if(unmatched.length>0){const setting=selection.source==="env"?"STACKS_MODEL_APIS":"security.api.models";log.warn(`[orm] ${setting} names ${unmatched.length===1?"a model":"models"} with no \`useApi\` trait, so no API was generated for ${unmatched.join(", ")}.`)}return selected})(),unscopedMutatingModels=[],contradictoryRoleGates=[];for(const[modelName,model]of Object.entries(apiModels)){const useApi=model.traits?.useApi;if(!useApi)continue;const apiConfig=typeof useApi==="object"?useApi:{},uri=apiConfig.uri||model.table||modelName.toLowerCase()+"s",enabledRoutes=apiConfig.routes||["index","show","store","update","destroy"],table=model.table||uri,fillableFields=getWritableFields(model),hiddenFields=getHiddenFields(model),basePath=apiBasePath(uri,apiConfig.prefix),readColumns=buildReadColumnMap(model.attributes,hiddenFields),{abilities,declared}=resolveAbilityMiddleware(useApi,model.dashboard);if(dashboardRoleMiddleware(model.dashboard)){const open=API_ABILITIES.filter((ability)=>enabledRoutes.includes(ability)&&abilities[ability].length===0);if(open.length>0)contradictoryRoleGates.push({model:modelName,abilities:open})}const hasMutating=["store","update","destroy"].some((r)=>enabledRoutes.includes(r)),openWrites=API_ABILITIES.filter((ability)=>ability!=="index"&&ability!=="show"&&enabledRoutes.includes(ability)&&abilities[ability].length===0);if(declared&&openWrites.length>0)log.warn(`[orm] ${modelName}: registering UNAUTHENTICATED ${openWrites.join(", ")} at ${basePath} (explicit empty \`middleware\` opt-out)`);const openReads=API_ABILITIES.filter((ability)=>(ability==="index"||ability==="show")&&enabledRoutes.includes(ability)&&abilities[ability].length===0);if(declared&&openReads.length>0)log.debug(`[orm] ${modelName}: registering public ${openReads.join(", ")} at ${basePath} (declared an empty read \`middleware\`)`);const rowScoped=!!effectiveOwnershipConfig(model),forceAuth=(names)=>rowScoped&&!names.includes("auth")?["auth",...names]:names,indexMiddleware=forceAuth(abilities.index),showMiddleware=forceAuth(abilities.show),declaredUnscoped=ownershipDeclaredUnscoped(model),writesAreUnscoped=hasMutating&&!rowScoped&&!declaredUnscoped;if(writesAreUnscoped)unscopedMutatingModels.push(modelName);if(hasMutating&&declaredUnscoped)log.debug(`[orm] ${modelName}: registering unscoped mutating routes at ${basePath} (declared \`ownership: false\`)`);const skipMutatingRoutes=writesAreUnscoped&&rowScopingPolicy==="deny",applyMiddleware=(chain,names)=>{let r=chain;for(const name of names)if(r&&typeof r.middleware==="function")r=r.middleware(name);return r};if(enabledRoutes.includes("index")&&!routeClaimed("GET",basePath))applyMiddleware(route.get(basePath,async(req)=>{try{const url=new URL(req.url),{page,perPage,offset}=resolveIndexPageArgs(url.searchParams),sort=url.searchParams.get("sort"),refusedList=await policyRefusal(modelName,"viewAny",req);if(refusedList)return refusedList;let query=dynamicDb().selectFrom(table);query=applySorting(query,sort,readColumns);const RESERVED=new Set(["page","per_page","sort","fields","search","include","limit","offset","with_count","withTrashed","onlyTrashed"]);for(const[key,value]of url.searchParams.entries()){if(RESERVED.has(key)||!value)continue;if(!/^[a-z_][a-z0-9_]*$/i.test(key))continue;const col=readColumns.get(key);if(col)query=query.where(col,"=",value)}const searchTerm=url.searchParams.get("search");if(searchTerm&&fillableFields.length>0){const textFields=fillableFields.slice(0,5);query=query.where((qb)=>{for(const field of textFields)qb=qb.orWhere(toSnakeCase(field),"like",`%${searchTerm}%`);return qb})}const fieldsParam=url.searchParams.get("fields");if(fieldsParam){const selectedFields=fieldsParam.split(",").map((f)=>f.trim()).filter((f)=>/^[a-z_][a-z0-9_]*$/i.test(f)).map((f)=>toSnakeCase(f));if(selectedFields.length>0)query=query.select(selectedFields)}const usesSoftDeletes=!!model.traits?.useSoftDeletes,withTrashed=url.searchParams.get("withTrashed")==="true",onlyTrashed=url.searchParams.get("onlyTrashed")==="true";if(usesSoftDeletes&&!withTrashed)query=onlyTrashed?query.whereNotNull("deleted_at"):query.whereNull("deleted_at");const own=rowScoped?await resolveOwnership(model,await authedUserFromRequest(req),req):{enforced:!1,value:null,field:"",bypass:!1};if(own.enforced&&!own.bypass){if(own.value==null||Array.isArray(own.value)&&own.value.length===0){const emptyPaginator=buildIndexPaginator(url,page,perPage,0,!1,void 0);return new Response(JSON.stringify({data:[],...emptyPaginator}),{status:200,headers:{"Content-Type":"application/json","Cache-Control":"private, no-store",Vary:"Authorization"}})}query=Array.isArray(own.value)?query.where(own.field,"in",own.value):query.where(own.field,"=",own.value)}const rawResults=await query.limit(perPage+1).offset(offset).get()||[],hasMore=rawResults.length>perPage;let records=(hasMore?rawResults.slice(0,perPage):rawResults).map((r)=>stripHidden(applyReadCasts(r,model),hiddenFields));const includeParam=url.searchParams.get("include");if(includeParam)records=await applyIncludes(records,model,includeParam,models);const wantCount=url.searchParams.get("with_count")==="true";let total;if(wantCount)try{let countQuery=dynamicDb().selectFrom(table);if(own.enforced&&!own.bypass&&own.value!=null)countQuery=Array.isArray(own.value)?countQuery.where(own.field,"in",own.value):countQuery.where(own.field,"=",own.value);const raw=await countQuery.count(),n=typeof raw==="number"?raw:Number(raw?.count??raw);if(Number.isFinite(n))total=n}catch{}const respHeaders={"Content-Type":"application/json"};if(total!==void 0&&!Number.isNaN(total))respHeaders["X-Total-Count"]=String(total);respHeaders["Cache-Control"]=own.enforced&&!own.bypass?"private, no-store":"public, max-age=15, must-revalidate";respHeaders.Vary="Authorization";const paginator=buildIndexPaginator(url,page,perPage,records.length,hasMore,total);return new Response(JSON.stringify({data:records,...paginator}),{status:200,headers:respHeaders})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to fetch ${uri}`,detail:String(err)},500)}}),indexMiddleware);if(enabledRoutes.includes("show")&&!routeClaimed("GET",`${basePath}/{id}`))applyMiddleware(route.get(`${basePath}/{id}`,async(req)=>{try{const id=coerceId(req.params?.id);if(id==null)return jsonResponse({error:"Invalid ID parameter"},400);const result=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!result)return jsonResponse({error:`${modelName} not found`},404);const url=new URL(req.url);if(model.traits?.useSoftDeletes&&result.deleted_at&&url.searchParams.get("withTrashed")!=="true")return jsonResponse({error:`${modelName} not found`},404);const own=rowScoped?await resolveOwnership(model,await authedUserFromRequest(req),req):{enforced:!1,value:null,field:"",bypass:!1};if(own.enforced&&!own.bypass){if(own.value==null||!ownsRow(result[own.field],own.value))return jsonResponse({error:`${modelName} not found`},404)}const refusedView=await policyRefusal(modelName,"view",req,result);if(refusedView)return refusedView;let payload=stripHidden(applyReadCasts(result,model),hiddenFields);const includeParam=url.searchParams.get("include");if(includeParam){const[withRel]=await applyIncludes([payload],model,includeParam,models);payload=withRel}const lastWrite=payload?.updated_at||payload?.created_at,etag=lastWrite?`W/"${payload.id}-${String(lastWrite)}"`:void 0,ifNoneMatch=req.headers?.get?.("if-none-match");if(etag&&ifNoneMatch&&ifNoneMatch===etag)return new Response(null,{status:304,headers:{ETag:etag}});const headers={"Content-Type":"application/json"};if(etag)headers.ETag=etag;headers["Cache-Control"]=own.enforced&&!own.bypass?"private, no-store":"public, max-age=30, must-revalidate";headers.Vary="Authorization";return new Response(JSON.stringify({data:payload}),{status:200,headers})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to fetch ${modelName}`,detail:String(err)},500)}}),showMiddleware);if(!skipMutatingRoutes&&enabledRoutes.includes("store")&&!routeClaimed("POST",basePath))applyMiddleware(route.post(basePath,async(req)=>{try{const body=await getRequestBody(req),safeBody=dropHiddenInputs(body,hiddenFields);let data=filterFillable(safeBody,fillableFields);const authedUser=await authedUserFromRequest(req);if(authedUser){const stamped=await resolveAuthedFill(model,"creating",authedUser,req);for(const[k,v]of Object.entries(stamped))if(v!==void 0&&v!==null&&data[k]===void 0)data[k]=v}const refusedCreate=await policyRefusal(modelName,"create",req);if(refusedCreate)return refusedCreate;const own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);const stamped=stampOwnership(data,own.field,own.value);if(stamped.error)return jsonResponse({error:stamped.error},403);data=stamped.data}if(Object.keys(data).length===0)return jsonResponse({error:"No fillable fields provided"},422);const v=validateWriteBody(data,model,"creating");if(!v.valid)return jsonResponse({success:!1,message:"Validation failed",errors:v.errors},422);if(model.traits?.useTimestamps!==!1){const now=new Date().toISOString();data.created_at=now;data.updated_at=now}const hookedData=await applyDefinedSetters(data,model),writeData=toSnakeCaseKeys(applySetCasts(hookedData,model)),result=await dynamicDb().insertInto(table).values(writeData).execute();let created=writeData;try{const lastId=result?.lastInsertRowid??result?.insertId??result;if(lastId){const fetched=await dynamicDb().selectFrom(table).where({id:lastId}).executeTakeFirst();if(fetched)created=fetched}}catch{}return jsonResponse({data:stripHidden(applyReadCasts(created,model),hiddenFields)},201)}catch(err){const{status,body}=mapWriteError(err,modelName,"create");return jsonResponse(body,status)}}),abilities.store);if(!skipMutatingRoutes&&enabledRoutes.includes("update")){const updateHandler=async(req)=>{try{const id=coerceId(req.params?.id);if(id==null)return jsonResponse({error:"Invalid ID parameter"},400);const body=await getRequestBody(req),safeBody=dropHiddenInputs(body,hiddenFields),data=filterFillable(safeBody,fillableFields),authedUser=await authedUserFromRequest(req);if(authedUser){const stamped=await resolveAuthedFill(model,"updating",authedUser,req);for(const[k,v]of Object.entries(stamped))if(v!==void 0&&v!==null&&data[k]===void 0)data[k]=v}if(Object.keys(data).length===0)return jsonResponse({error:"No fillable fields provided"},422);const v=validateWriteBody(data,model,"updating");if(!v.valid)return jsonResponse({success:!1,message:"Validation failed",errors:v.errors},422);const existing=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!existing)return jsonResponse({error:`${modelName} not found`},404);const refusedUpdate=await policyRefusal(modelName,"update",req,existing);if(refusedUpdate)return refusedUpdate;const own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);const rowOwner=existing[own.field];if(!ownsRow(rowOwner,own.value))return jsonResponse({error:`Not your ${modelName}`},403);const submittedOwnerKey=Object.keys(data).find((k)=>toSnakeCase(k)===toSnakeCase(own.field));if(submittedOwnerKey!==void 0&&!ownsRow(data[submittedOwnerKey],own.value))return jsonResponse({error:`Cannot reassign ${modelName} ownership`},403)}if(model.traits?.useTimestamps!==!1)data.updated_at=new Date().toISOString();const hookedData=await applyDefinedSetters(data,model),writeData=toSnakeCaseKeys(applySetCasts(hookedData,model));await dynamicDb().updateTable(table).set(writeData).where({id}).execute();let updated={...existing,...writeData};try{const fetched=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(fetched)updated=fetched}catch{}return jsonResponse({data:stripHidden(applyReadCasts(updated,model),hiddenFields)})}catch(err){const{status,body}=mapWriteError(err,modelName,"update");return jsonResponse(body,status)}};if(!routeClaimed("PUT",`${basePath}/{id}`))applyMiddleware(route.put(`${basePath}/{id}`,updateHandler),abilities.update);if(!routeClaimed("PATCH",`${basePath}/{id}`))applyMiddleware(route.patch(`${basePath}/{id}`,updateHandler),abilities.update)}const usesSoftDeletes=!!model.traits?.useSoftDeletes;if(!skipMutatingRoutes&&enabledRoutes.includes("destroy")&&!routeClaimed("DELETE",`${basePath}/{id}`))applyMiddleware(route.delete(`${basePath}/{id}`,async(req)=>{try{const id=coerceId(req.params?.id);if(id==null)return jsonResponse({error:"Invalid ID parameter"},400);const existing=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!existing)return jsonResponse({error:`${modelName} not found`},404);const authedUser=await authedUserFromRequest(req),own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);const rowOwner=existing[own.field];if(!ownsRow(rowOwner,own.value))return jsonResponse({error:`Not your ${modelName}`},403)}const refusedDelete=await policyRefusal(modelName,"delete",req,existing);if(refusedDelete)return refusedDelete;if(usesSoftDeletes){const now=new Date().toISOString();await dynamicDb().updateTable(table).set({deleted_at:now,updated_at:now}).where({id}).execute()}else await dynamicDb().deleteFrom(table).where({id}).execute();return new Response(null,{status:204})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to delete ${modelName}`,detail:String(err)},500)}}),abilities.destroy);if(!skipMutatingRoutes&&enabledRoutes.includes("destroy")&&!routeClaimed("POST",`${basePath}/bulk-delete`))applyMiddleware(route.post(`${basePath}/bulk-delete`,async(req)=>{try{const ids=(await getRequestBody(req))?.ids;if(!Array.isArray(ids)||ids.length===0)return jsonResponse({error:"An array of IDs is required"},422);if(ids.length>100)return jsonResponse({error:"Cannot delete more than 100 records at once"},422);const validIds=[],invalidIds=[];for(const raw of ids){const coerced=coerceId(raw);if(coerced!=null)validIds.push(coerced);else invalidIds.push(raw)}if(invalidIds.length>0)return jsonResponse({error:"Invalid IDs in batch",invalid:invalidIds},422);const authedUser=await authedUserFromRequest(req),own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);if(own.value==null)return jsonResponse({error:"Caller has no ownership identity"},403);const rows=await dynamicDb().selectFrom(table).select(["id",own.field]).execute(),ownedIds=new Set(rows.filter((r)=>ownsRow(r[own.field],own.value)).map((r)=>String(r.id))),notOwned=validIds.filter((id)=>!ownedIds.has(String(id)));if(notOwned.length>0)return jsonResponse({error:`Cannot delete ${modelName} you don't own`,ids:notOwned},403)}if(await hasPolicyFor(modelName)){const refusedRows=[];for(const id of validIds){const row=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!row)continue;if(await policyRefusal(modelName,"delete",req,row))refusedRows.push(id)}if(refusedRows.length>0)return jsonResponse({error:`Cannot delete ${modelName} this policy refuses`,ids:refusedRows},403)}const now=new Date().toISOString();for(const id of validIds)if(usesSoftDeletes)await dynamicDb().updateTable(table).set({deleted_at:now,updated_at:now}).where({id}).execute();else await dynamicDb().deleteFrom(table).where({id}).execute();return jsonResponse({message:`Successfully deleted ${validIds.length} ${uri}`})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to bulk delete ${uri}`,detail:String(err)},500)}}),abilities.destroy)}{const report=describeUnscopedMutatingModels(unscopedMutatingModels,rowScopingPolicy);if(report)log.warn(report);const contradictions=describeContradictoryRoleGates(contradictoryRoleGates);if(contradictions)log.warn(contradictions)}export default route;
1
+ import process from"node:process";import{dialectCapabilities,sqlDateTime}from"@stacksjs/database/runtime";import{parseBearerToken,route}from"@stacksjs/router";import{projectPath,storagePath}from"@stacksjs/path";import{createQueryBuilder,config as queryBuilderConfig,setConfig}from"@stacksjs/query-builder";import{HttpError}from"@stacksjs/error-handling";import{log}from"@stacksjs/logging/runtime";import{API_ABILITIES,apiBasePath,applyCasts,applySorting,buildIndexPaginator,buildReadColumnMap,dashboardRoleMiddleware,describeContradictoryRoleGates,describeUnscopedMutatingModels,dropHiddenInputs,filterFillable,getWritableFields,indexRouteShapes,mapWriteError,ownershipDeclaredUnscoped,policyDecision,resolveAbilityMiddleware,resolveIndexPageArgs,resolveRowScopingPolicy,routeShapeKey,stampOwnership,stripHidden,toSnakeCase,toSnakeCaseKeys,validateWriteBody}from"./auto-crud";import{loadModelRegistryWithOrigins,resolveModelApiSelection,selectApiModels}from"./model-registry";import{effectiveOwnershipConfig}from"./ownership";import{connectionConflictWarning,splitProjectQueryBuilderConfig}from"./project-query-builder-config";const qbConfigPath=projectPath("config/qb.ts");let projectQbConfig;try{projectQbConfig=(await import(qbConfigPath)).default}catch{log.debug("[orm] No config/qb.ts override found - using the connection from config/database.ts")}if(projectQbConfig){const{behaviour,conflicts}=splitProjectQueryBuilderConfig(projectQbConfig,queryBuilderConfig);if(conflicts.length)log.warn(connectionConflictWarning(conflicts));setConfig(behaviour)}const registry=await loadModelRegistryWithOrigins({defaultsRoot:storagePath("framework/defaults/app/Models"),userRoot:projectPath("app/Models"),onImportError:(file,error)=>log.warn(`[orm] Could not load model ${file}: ${error.message}`)}),models=registry.models;function dynamicDb(){return createQueryBuilder()}function writeTimestamp(){return dialectCapabilities(queryBuilderConfig.dialect).wire==="mysql"?sqlDateTime():new Date().toISOString()}const routeShapeIndex=indexRouteShapes(route.routes);function routeClaimed(method,path){const key=routeShapeKey(method,path),existing=routeShapeIndex.get(key);if(!existing){routeShapeIndex.set(key,{method,path});return!1}const existingPath=String(existing.path??"");log.debug(`[orm] Skipping generated ${method} ${path} - already defined${existingPath&&existingPath!==path?` as ${existingPath}`:""}.`);return!0}function getHiddenFields(model){if(!model.attributes)return[];return Object.entries(model.attributes).filter(([_,attr])=>attr.hidden===!0).map(([name])=>name)}function pluralize(s){if(s.endsWith("y")&&!/[aeiou]y$/i.test(s))return`${s.slice(0,-1)}ies`;if(/(?:s|x|z|ch|sh)$/i.test(s))return`${s}es`;return`${s}s`}async function applyIncludes(rows,model,includeParam,modelsRegistry){if(!rows.length)return rows;const requested=includeParam.split(",").map((s)=>s.trim()).filter(Boolean);if(!requested.length)return rows;const belongsTo=Array.isArray(model.belongsTo)?model.belongsTo:[],hasMany=Array.isArray(model.hasMany)?model.hasMany:[],hasOne=Array.isArray(model.hasOne)?model.hasOne:[],allowed=new Map;for(const m of belongsTo)allowed.set(toSnakeCase(m),{kind:"belongsTo",modelName:m});for(const m of hasOne)allowed.set(toSnakeCase(m),{kind:"hasOne",modelName:m});for(const m of hasMany)allowed.set(pluralize(toSnakeCase(m)),{kind:"hasMany",modelName:m});for(const include of requested){const meta=allowed.get(include);if(!meta)continue;const related=modelsRegistry[meta.modelName];if(!related)continue;const relatedTable=related.table||pluralize(toSnakeCase(meta.modelName)),relatedHidden=getHiddenFields(related),relatedAttrs=(id)=>stripHidden(applyReadCasts(id,related),relatedHidden);if(meta.kind==="belongsTo"||meta.kind==="hasOne"){const fkOnParent=`${toSnakeCase(meta.modelName)}_id`,ids=[...new Set(rows.map((r)=>r[fkOnParent]).filter((v)=>v!=null))];if(!ids.length){for(const r of rows)r[include]=null;continue}const childRows=await dynamicDb().selectFrom(relatedTable).whereIn("id",ids).get(),byId=new Map;for(const cr of childRows??[])byId.set(String(cr.id),relatedAttrs(cr));for(const r of rows)r[include]=byId.get(String(r[fkOnParent]))??null}else{const parentName=String(model.name??""),fkOnChild=`${toSnakeCase(parentName)}_id`,parentIds=[...new Set(rows.map((r)=>r.id).filter((v)=>v!=null))];if(!parentIds.length){for(const r of rows)r[include]=[];continue}const childRows=await dynamicDb().selectFrom(relatedTable).whereIn(fkOnChild,parentIds).get(),grouped=new Map;for(const cr of childRows??[]){const key=String(cr[fkOnChild]),arr=grouped.get(key)??[];arr.push(relatedAttrs(cr));grouped.set(key,arr)}for(const r of rows)r[include]=grouped.get(String(r.id))??[]}}return rows}async function applyDefinedSetters(data,model){const setters=model?.set;if(!setters||typeof setters!=="object")return data;const out={...data};for(const[key,fn]of Object.entries(setters)){if(typeof fn!=="function"||!(key in out))continue;try{out[key]=await fn(out)}catch{}}return out}function applySetCasts(data,model){return applyCasts(data,model?.casts,"set")}function applyReadCasts(row,model){return applyCasts(row,model?.casts,"get")}function jsonResponse(data,status=200){return new Response(JSON.stringify(data),{status,headers:{"Content-Type":"application/json"}})}function errorResponse(req,body,status=400){const reqId=req?._requestId,enriched=reqId?{...body,request_id:reqId}:body;return new Response(JSON.stringify(enriched),{status,headers:{"Content-Type":"application/json"}})}const MAX_BODY_BYTES=1048576;async function getRequestBody(req){if(Number(req.headers?.get?.("content-length")??0)>MAX_BODY_BYTES)throw new HttpError(413,`Request body exceeds ${MAX_BODY_BYTES}-byte limit`);if(req.jsonBody&&typeof req.jsonBody==="object")return req.jsonBody;if(req.formBody&&typeof req.formBody==="object")return req.formBody;try{const text=await req.clone().text();if(text.length>MAX_BODY_BYTES)throw new HttpError(413,`Request body exceeds ${MAX_BODY_BYTES}-byte limit`);if(!text)return{};const parsed=JSON.parse(text);return parsed&&typeof parsed==="object"&&!Array.isArray(parsed)?parsed:{}}catch(err){if(err instanceof HttpError)throw err;throw new HttpError(400,`Invalid JSON body: ${err.message}`)}}function coerceId(raw){if(raw==null)return null;if(typeof raw==="number")return Number.isFinite(raw)&&raw>0?raw:null;if(typeof raw==="string"){const trimmed=raw.trim();if(!trimmed)return null;if(/^\d+$/.test(trimmed)){const n=Number(trimmed);return Number.isFinite(n)&&n>0?n:null}return trimmed}return null}function bearerOf(req){const fn=req.bearerToken;if(typeof fn==="function"){const t=fn.call(req);if(t)return t}return parseBearerToken(req.headers?.get?.("authorization")??null)}async function authedUserFromRequest(req){const stored=req._authenticatedUser;if(stored)return stored;const token=bearerOf(req);if(!token)return null;try{const{Auth}=await import("@stacksjs/auth"),user=await Auth.getUserFromToken(token);if(user)req._authenticatedUser=user;return user||null}catch{return null}}let authGateModule;async function authGate(){try{const{Gate}=authGateModule??=await import("@stacksjs/auth");return Gate}catch{return null}}async function hasPolicyFor(modelName){const gate=await authGate();return!!gate&&gate.hasPolicy(modelName)}async function policyRefusal(modelName,ability,req,subject){const refusal=await policyDecision(modelName,ability,{gate:authGate,user:()=>authedUserFromRequest(req)},subject);return refusal?jsonResponse({error:refusal.message},refusal.status):null}function ownsRow(rowField,ownerValue){if(rowField==null||ownerValue==null)return!1;if(Array.isArray(ownerValue))return ownerValue.some((v)=>String(v)===String(rowField));return String(rowField)===String(ownerValue)}async function resolveOwnership(model,user,req){const cfg=effectiveOwnershipConfig(model);if(!cfg||!cfg.field||typeof cfg.resolve!=="function")return{enforced:!1,value:null,field:"",bypass:!1};const bypass=typeof cfg.bypass==="function"?!!cfg.bypass(user,req):!1;let value=null;try{value=await cfg.resolve(user,req)}catch{value=null}return{enforced:!0,value,field:String(cfg.field),bypass}}async function resolveAuthedFill(model,hook,user,req){const cfg=model?.authedFill?.[hook];if(!cfg)return{};if(typeof cfg==="function")try{const result=await cfg(user,req);return result&&typeof result==="object"?result:{}}catch{return{}}if(typeof cfg==="object"){const out={};for(const[field,getter]of Object.entries(cfg))try{out[field]=typeof getter==="function"?await getter(user,req):getter}catch{}return out}return{}}const securityApiConfig=await(async()=>{try{return(await import(projectPath("config/security.ts"))).default?.api}catch{return}})(),rowScopingPolicy=resolveRowScopingPolicy(securityApiConfig?.rowScoping,process.env.STACKS_API_ROW_SCOPING),apiModels=(()=>{const selection=resolveModelApiSelection(securityApiConfig?.models,process.env.STACKS_MODEL_APIS),{models:selected,unmatched}=selectApiModels(registry,selection);if(unmatched.length>0){const setting=selection.source==="env"?"STACKS_MODEL_APIS":"security.api.models";log.warn(`[orm] ${setting} names ${unmatched.length===1?"a model":"models"} with no \`useApi\` trait, so no API was generated for ${unmatched.join(", ")}.`)}return selected})(),unscopedMutatingModels=[],contradictoryRoleGates=[];for(const[modelName,model]of Object.entries(apiModels)){const useApi=model.traits?.useApi;if(!useApi)continue;const apiConfig=typeof useApi==="object"?useApi:{},uri=apiConfig.uri||model.table||modelName.toLowerCase()+"s",enabledRoutes=apiConfig.routes||["index","show","store","update","destroy"],table=model.table||uri,fillableFields=getWritableFields(model),hiddenFields=getHiddenFields(model),basePath=apiBasePath(uri,apiConfig.prefix),readColumns=buildReadColumnMap(model.attributes,hiddenFields),{abilities,declared}=resolveAbilityMiddleware(useApi,model.dashboard);if(dashboardRoleMiddleware(model.dashboard)){const open=API_ABILITIES.filter((ability)=>enabledRoutes.includes(ability)&&abilities[ability].length===0);if(open.length>0)contradictoryRoleGates.push({model:modelName,abilities:open})}const hasMutating=["store","update","destroy"].some((r)=>enabledRoutes.includes(r)),openWrites=API_ABILITIES.filter((ability)=>ability!=="index"&&ability!=="show"&&enabledRoutes.includes(ability)&&abilities[ability].length===0);if(declared&&openWrites.length>0)log.warn(`[orm] ${modelName}: registering UNAUTHENTICATED ${openWrites.join(", ")} at ${basePath} (explicit empty \`middleware\` opt-out)`);const openReads=API_ABILITIES.filter((ability)=>(ability==="index"||ability==="show")&&enabledRoutes.includes(ability)&&abilities[ability].length===0);if(declared&&openReads.length>0)log.debug(`[orm] ${modelName}: registering public ${openReads.join(", ")} at ${basePath} (declared an empty read \`middleware\`)`);const rowScoped=!!effectiveOwnershipConfig(model),forceAuth=(names)=>rowScoped&&!names.includes("auth")?["auth",...names]:names,indexMiddleware=forceAuth(abilities.index),showMiddleware=forceAuth(abilities.show),declaredUnscoped=ownershipDeclaredUnscoped(model),writesAreUnscoped=hasMutating&&!rowScoped&&!declaredUnscoped;if(writesAreUnscoped)unscopedMutatingModels.push(modelName);if(hasMutating&&declaredUnscoped)log.debug(`[orm] ${modelName}: registering unscoped mutating routes at ${basePath} (declared \`ownership: false\`)`);const skipMutatingRoutes=writesAreUnscoped&&rowScopingPolicy==="deny",applyMiddleware=(chain,names)=>{let r=chain;for(const name of names)if(r&&typeof r.middleware==="function")r=r.middleware(name);return r};if(enabledRoutes.includes("index")&&!routeClaimed("GET",basePath))applyMiddleware(route.get(basePath,async(req)=>{try{const url=new URL(req.url),{page,perPage,offset}=resolveIndexPageArgs(url.searchParams),sort=url.searchParams.get("sort"),refusedList=await policyRefusal(modelName,"viewAny",req);if(refusedList)return refusedList;let query=dynamicDb().selectFrom(table);query=applySorting(query,sort,readColumns);const RESERVED=new Set(["page","per_page","sort","fields","search","include","limit","offset","with_count","withTrashed","onlyTrashed"]);for(const[key,value]of url.searchParams.entries()){if(RESERVED.has(key)||!value)continue;if(!/^[a-z_][a-z0-9_]*$/i.test(key))continue;const col=readColumns.get(key);if(col)query=query.where(col,"=",value)}const searchTerm=url.searchParams.get("search");if(searchTerm&&fillableFields.length>0){const textFields=fillableFields.slice(0,5);query=query.where((qb)=>{for(const field of textFields)qb=qb.orWhere(toSnakeCase(field),"like",`%${searchTerm}%`);return qb})}const fieldsParam=url.searchParams.get("fields");if(fieldsParam){const selectedFields=fieldsParam.split(",").map((f)=>f.trim()).filter((f)=>/^[a-z_][a-z0-9_]*$/i.test(f)).map((f)=>toSnakeCase(f));if(selectedFields.length>0)query=query.select(selectedFields)}const usesSoftDeletes=!!model.traits?.useSoftDeletes,withTrashed=url.searchParams.get("withTrashed")==="true",onlyTrashed=url.searchParams.get("onlyTrashed")==="true";if(usesSoftDeletes&&!withTrashed)query=onlyTrashed?query.whereNotNull("deleted_at"):query.whereNull("deleted_at");const own=rowScoped?await resolveOwnership(model,await authedUserFromRequest(req),req):{enforced:!1,value:null,field:"",bypass:!1};if(own.enforced&&!own.bypass){if(own.value==null||Array.isArray(own.value)&&own.value.length===0){const emptyPaginator=buildIndexPaginator(url,page,perPage,0,!1,void 0);return new Response(JSON.stringify({data:[],...emptyPaginator}),{status:200,headers:{"Content-Type":"application/json","Cache-Control":"private, no-store",Vary:"Authorization"}})}query=Array.isArray(own.value)?query.where(own.field,"in",own.value):query.where(own.field,"=",own.value)}const rawResults=await query.limit(perPage+1).offset(offset).get()||[],hasMore=rawResults.length>perPage;let records=(hasMore?rawResults.slice(0,perPage):rawResults).map((r)=>stripHidden(applyReadCasts(r,model),hiddenFields));const includeParam=url.searchParams.get("include");if(includeParam)records=await applyIncludes(records,model,includeParam,models);const wantCount=url.searchParams.get("with_count")==="true";let total;if(wantCount)try{let countQuery=dynamicDb().selectFrom(table);if(own.enforced&&!own.bypass&&own.value!=null)countQuery=Array.isArray(own.value)?countQuery.where(own.field,"in",own.value):countQuery.where(own.field,"=",own.value);const raw=await countQuery.count(),n=typeof raw==="number"?raw:Number(raw?.count??raw);if(Number.isFinite(n))total=n}catch{}const respHeaders={"Content-Type":"application/json"};if(total!==void 0&&!Number.isNaN(total))respHeaders["X-Total-Count"]=String(total);respHeaders["Cache-Control"]=own.enforced&&!own.bypass?"private, no-store":"public, max-age=15, must-revalidate";respHeaders.Vary="Authorization";const paginator=buildIndexPaginator(url,page,perPage,records.length,hasMore,total);return new Response(JSON.stringify({data:records,...paginator}),{status:200,headers:respHeaders})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to fetch ${uri}`,detail:String(err)},500)}}),indexMiddleware);if(enabledRoutes.includes("show")&&!routeClaimed("GET",`${basePath}/{id}`))applyMiddleware(route.get(`${basePath}/{id}`,async(req)=>{try{const id=coerceId(req.params?.id);if(id==null)return jsonResponse({error:"Invalid ID parameter"},400);const result=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!result)return jsonResponse({error:`${modelName} not found`},404);const url=new URL(req.url);if(model.traits?.useSoftDeletes&&result.deleted_at&&url.searchParams.get("withTrashed")!=="true")return jsonResponse({error:`${modelName} not found`},404);const own=rowScoped?await resolveOwnership(model,await authedUserFromRequest(req),req):{enforced:!1,value:null,field:"",bypass:!1};if(own.enforced&&!own.bypass){if(own.value==null||!ownsRow(result[own.field],own.value))return jsonResponse({error:`${modelName} not found`},404)}const refusedView=await policyRefusal(modelName,"view",req,result);if(refusedView)return refusedView;let payload=stripHidden(applyReadCasts(result,model),hiddenFields);const includeParam=url.searchParams.get("include");if(includeParam){const[withRel]=await applyIncludes([payload],model,includeParam,models);payload=withRel}const lastWrite=payload?.updated_at||payload?.created_at,etag=lastWrite?`W/"${payload.id}-${String(lastWrite)}"`:void 0,ifNoneMatch=req.headers?.get?.("if-none-match");if(etag&&ifNoneMatch&&ifNoneMatch===etag)return new Response(null,{status:304,headers:{ETag:etag}});const headers={"Content-Type":"application/json"};if(etag)headers.ETag=etag;headers["Cache-Control"]=own.enforced&&!own.bypass?"private, no-store":"public, max-age=30, must-revalidate";headers.Vary="Authorization";return new Response(JSON.stringify({data:payload}),{status:200,headers})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to fetch ${modelName}`,detail:String(err)},500)}}),showMiddleware);if(!skipMutatingRoutes&&enabledRoutes.includes("store")&&!routeClaimed("POST",basePath))applyMiddleware(route.post(basePath,async(req)=>{try{const body=await getRequestBody(req),safeBody=dropHiddenInputs(body,hiddenFields);let data=filterFillable(safeBody,fillableFields);const authedUser=await authedUserFromRequest(req);if(authedUser){const stamped=await resolveAuthedFill(model,"creating",authedUser,req);for(const[k,v]of Object.entries(stamped))if(v!==void 0&&v!==null&&data[k]===void 0)data[k]=v}const refusedCreate=await policyRefusal(modelName,"create",req);if(refusedCreate)return refusedCreate;const own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);const stamped=stampOwnership(data,own.field,own.value);if(stamped.error)return jsonResponse({error:stamped.error},403);data=stamped.data}if(Object.keys(data).length===0)return jsonResponse({error:"No fillable fields provided"},422);const v=validateWriteBody(data,model,"creating");if(!v.valid)return jsonResponse({success:!1,message:"Validation failed",errors:v.errors},422);if(model.traits?.useUuid&&!data.uuid)data.uuid=crypto.randomUUID();if(model.traits?.useTimestamps!==!1){const now=writeTimestamp();data.created_at=now;data.updated_at=now}const hookedData=await applyDefinedSetters(data,model),writeData=toSnakeCaseKeys(applySetCasts(hookedData,model)),result=await dynamicDb().insertInto(table).values(writeData).execute();let created=writeData;try{const lastId=result?.lastInsertRowid??result?.insertId??result;if(lastId){const fetched=await dynamicDb().selectFrom(table).where({id:lastId}).executeTakeFirst();if(fetched)created=fetched}}catch{}return jsonResponse({data:stripHidden(applyReadCasts(created,model),hiddenFields)},201)}catch(err){const{status,body}=mapWriteError(err,modelName,"create");return jsonResponse(body,status)}}),abilities.store);if(!skipMutatingRoutes&&enabledRoutes.includes("update")){const updateHandler=async(req)=>{try{const id=coerceId(req.params?.id);if(id==null)return jsonResponse({error:"Invalid ID parameter"},400);const body=await getRequestBody(req),safeBody=dropHiddenInputs(body,hiddenFields),data=filterFillable(safeBody,fillableFields),authedUser=await authedUserFromRequest(req);if(authedUser){const stamped=await resolveAuthedFill(model,"updating",authedUser,req);for(const[k,v]of Object.entries(stamped))if(v!==void 0&&v!==null&&data[k]===void 0)data[k]=v}if(Object.keys(data).length===0)return jsonResponse({error:"No fillable fields provided"},422);const v=validateWriteBody(data,model,"updating");if(!v.valid)return jsonResponse({success:!1,message:"Validation failed",errors:v.errors},422);const existing=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!existing)return jsonResponse({error:`${modelName} not found`},404);const refusedUpdate=await policyRefusal(modelName,"update",req,existing);if(refusedUpdate)return refusedUpdate;const own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);const rowOwner=existing[own.field];if(!ownsRow(rowOwner,own.value))return jsonResponse({error:`Not your ${modelName}`},403);const submittedOwnerKey=Object.keys(data).find((k)=>toSnakeCase(k)===toSnakeCase(own.field));if(submittedOwnerKey!==void 0&&!ownsRow(data[submittedOwnerKey],own.value))return jsonResponse({error:`Cannot reassign ${modelName} ownership`},403)}if(model.traits?.useTimestamps!==!1)data.updated_at=writeTimestamp();const hookedData=await applyDefinedSetters(data,model),writeData=toSnakeCaseKeys(applySetCasts(hookedData,model));await dynamicDb().updateTable(table).set(writeData).where({id}).execute();let updated={...existing,...writeData};try{const fetched=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(fetched)updated=fetched}catch{}return jsonResponse({data:stripHidden(applyReadCasts(updated,model),hiddenFields)})}catch(err){const{status,body}=mapWriteError(err,modelName,"update");return jsonResponse(body,status)}};if(!routeClaimed("PUT",`${basePath}/{id}`))applyMiddleware(route.put(`${basePath}/{id}`,updateHandler),abilities.update);if(!routeClaimed("PATCH",`${basePath}/{id}`))applyMiddleware(route.patch(`${basePath}/{id}`,updateHandler),abilities.update)}const usesSoftDeletes=!!model.traits?.useSoftDeletes;if(!skipMutatingRoutes&&enabledRoutes.includes("destroy")&&!routeClaimed("DELETE",`${basePath}/{id}`))applyMiddleware(route.delete(`${basePath}/{id}`,async(req)=>{try{const id=coerceId(req.params?.id);if(id==null)return jsonResponse({error:"Invalid ID parameter"},400);const existing=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!existing)return jsonResponse({error:`${modelName} not found`},404);const authedUser=await authedUserFromRequest(req),own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);const rowOwner=existing[own.field];if(!ownsRow(rowOwner,own.value))return jsonResponse({error:`Not your ${modelName}`},403)}const refusedDelete=await policyRefusal(modelName,"delete",req,existing);if(refusedDelete)return refusedDelete;if(usesSoftDeletes){const now=writeTimestamp();await dynamicDb().updateTable(table).set({deleted_at:now,updated_at:now}).where({id}).execute()}else await dynamicDb().deleteFrom(table).where({id}).execute();return new Response(null,{status:204})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to delete ${modelName}`,detail:String(err)},500)}}),abilities.destroy);if(!skipMutatingRoutes&&enabledRoutes.includes("destroy")&&!routeClaimed("POST",`${basePath}/bulk-delete`))applyMiddleware(route.post(`${basePath}/bulk-delete`,async(req)=>{try{const ids=(await getRequestBody(req))?.ids;if(!Array.isArray(ids)||ids.length===0)return jsonResponse({error:"An array of IDs is required"},422);if(ids.length>100)return jsonResponse({error:"Cannot delete more than 100 records at once"},422);const validIds=[],invalidIds=[];for(const raw of ids){const coerced=coerceId(raw);if(coerced!=null)validIds.push(coerced);else invalidIds.push(raw)}if(invalidIds.length>0)return jsonResponse({error:"Invalid IDs in batch",invalid:invalidIds},422);const authedUser=await authedUserFromRequest(req),own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);if(own.value==null)return jsonResponse({error:"Caller has no ownership identity"},403);const rows=await dynamicDb().selectFrom(table).select(["id",own.field]).execute(),ownedIds=new Set(rows.filter((r)=>ownsRow(r[own.field],own.value)).map((r)=>String(r.id))),notOwned=validIds.filter((id)=>!ownedIds.has(String(id)));if(notOwned.length>0)return jsonResponse({error:`Cannot delete ${modelName} you don't own`,ids:notOwned},403)}if(await hasPolicyFor(modelName)){const refusedRows=[];for(const id of validIds){const row=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!row)continue;if(await policyRefusal(modelName,"delete",req,row))refusedRows.push(id)}if(refusedRows.length>0)return jsonResponse({error:`Cannot delete ${modelName} this policy refuses`,ids:refusedRows},403)}const now=writeTimestamp();for(const id of validIds)if(usesSoftDeletes)await dynamicDb().updateTable(table).set({deleted_at:now,updated_at:now}).where({id}).execute();else await dynamicDb().deleteFrom(table).where({id}).execute();return jsonResponse({message:`Successfully deleted ${validIds.length} ${uri}`})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to bulk delete ${uri}`,detail:String(err)},500)}}),abilities.destroy)}{const report=describeUnscopedMutatingModels(unscopedMutatingModels,rowScopingPolicy);if(report)log.warn(report);const contradictions=describeContradictoryRoleGates(contradictoryRoleGates);if(contradictions)log.warn(contradictions)}export default route;
@@ -1 +1 @@
1
- import{mutationCount,sqlDateTime}from"@stacksjs/database/runtime";import{log}from"@stacksjs/logging";const DELETED_AT_COLUMN="deleted_at";export function createSoftDeleteMethods(model,primaryKey="id"){return{async softDelete(id){const now=sqlDateTime(),q=model.where(primaryKey,id);if(typeof q?.update==="function"){await q.update({[DELETED_AT_COLUMN]:now});return!0}return!1},async restore(id){const q=model.where(primaryKey,id).withTrashed();if(typeof q?.update==="function"){await q.update({[DELETED_AT_COLUMN]:null});return!0}return!1},async forceDelete(id){const q=model.where(primaryKey,id),scoped=typeof q?.withTrashed==="function"?q.withTrashed():q;if(typeof scoped?.forceDelete==="function")return mutationCount(await scoped.forceDelete())>0;return!1},withTrashed(){return typeof model.query==="function"?model.query():model},onlyTrashed(){const q=typeof model.query==="function"?model.query():model;if(typeof q?.whereNotNull==="function")return q.whereNotNull(DELETED_AT_COLUMN);return q?.where?.(DELETED_AT_COLUMN,"!=",null)??q}}}export function resolveSoftDeleteOptions(value){if(value===!0||value==null)return{};if(typeof value==="object"&&!Array.isArray(value)){const v=value;if(Array.isArray(v.cascade)){const cascade=v.cascade.filter((x)=>typeof x==="string");return cascade.length?{cascade}:{}}return{}}return{}}function resolveChildModel(parentDefinition,relationName){const hasMany=parentDefinition.hasMany??[],hasOne=parentDefinition.hasOne??[],isHasMany=hasMany.some((c)=>relationMatches(c,relationName,!0)),isHasOne=hasOne.some((c)=>relationMatches(c,relationName,!1));if(!isHasMany&&!isHasOne){log.warn(`[orm] soft-delete cascade: '${relationName}' is not a declared hasMany/hasOne relation - skipping`);return null}const className=(isHasMany?hasMany:hasOne).find((c)=>relationMatches(c,relationName,isHasMany))??null;if(!className)return null;const model=globalThis[className];if(!model){log.warn(`[orm] soft-delete cascade: model '${className}' is not yet on globalThis - skipping cascade for '${relationName}'`);return null}return{className,model}}function relationMatches(className,relationName,plural){const base=className.toLowerCase();if(relationName===base)return!0;if(plural&&relationName===`${base}s`)return!0;if(plural&&base.endsWith("y")&&relationName===`${base.slice(0,-1)}ies`)return!0;return!1}async function cascadeChildren(parentClassName,parentId,childModel,foreignKey,action){try{const where=childModel.where;if(typeof where!=="function"){log.warn(`[orm] cascade ${action}: child model has no .where()`);return}if(action==="softDelete"){const childSoftDelete=childModel.softDelete;if(typeof childSoftDelete==="function"){const q=where.call(childModel,foreignKey,parentId),rows=typeof q?.get==="function"?await q.get():Array.isArray(q)?q:[],childPk=childModel.primaryKey||"id";for(const r of rows){const id=r?.[childPk];if(id!=null)await childSoftDelete.call(childModel,id)}return}const q=where.call(childModel,foreignKey,parentId);if(typeof q?.delete==="function"){log.debug(`[orm] cascade soft-delete: child has no softDeletes trait, hard-deleting from ${String(childModel.table??"?")}`);await q.delete();return}}else{const childRestore=childModel.restore;if(typeof childRestore!=="function"){log.debug("[orm] cascade restore: child has no restore() - nothing to do");return}const withTrashed=childModel.withTrashed,q=(typeof withTrashed==="function"?withTrashed.call(childModel):childModel).where?.(foreignKey,parentId),rows=typeof q?.get==="function"?await q.get():[],childPk=childModel.primaryKey||"id";for(const r of rows){const id=r?.[childPk];if(id!=null)await childRestore.call(childModel,id)}}}catch(err){log.warn(`[orm] soft-delete cascade ${action} on ${parentClassName}#${String(parentId)} failed`,{error:err});if(process.env.STACKS_ORM_CASCADE_SWALLOW!=="true")throw err}}export async function cascadeSoftDelete(parentDefinition,options,parentId,action){const cascade=options.cascade;if(!cascade||cascade.length===0)return;const parentFk=`${parentDefinition.name.toLowerCase()}_id`;for(const relationName of cascade){const resolved=resolveChildModel(parentDefinition,relationName);if(!resolved)continue;await cascadeChildren(resolved.className,parentId,resolved.model,parentFk,action)}}
1
+ import{mutationCount,sqlDateTime}from"@stacksjs/database/runtime";import{log}from"@stacksjs/logging";const DELETED_AT_COLUMN="deleted_at";export function createSoftDeleteMethods(model,primaryKey="id"){return{async softDelete(id){const now=sqlDateTime(),q=model.where(primaryKey,id);if(typeof q?.update==="function"){await q.update({[DELETED_AT_COLUMN]:now});return!0}return!1},async restore(id){const q=model.where(primaryKey,id).withTrashed();if(typeof q?.update==="function"){await q.update({[DELETED_AT_COLUMN]:null});return!0}return!1},async forceDelete(id){const q=model.where(primaryKey,id),scoped=typeof q?.withTrashed==="function"?q.withTrashed():q;if(typeof scoped?.forceDelete==="function")return mutationCount(await scoped.forceDelete())>0;return!1},withTrashed(){const q=typeof model.query==="function"?model.query():model;return typeof q?.withTrashed==="function"?q.withTrashed():q},onlyTrashed(){const q=typeof model.query==="function"?model.query():model;if(typeof q?.onlyTrashed==="function")return q.onlyTrashed();if(typeof q?.whereNotNull==="function")return q.whereNotNull(DELETED_AT_COLUMN);return q?.where?.(DELETED_AT_COLUMN,"!=",null)??q}}}export function resolveSoftDeleteOptions(value){if(value===!0||value==null)return{};if(typeof value==="object"&&!Array.isArray(value)){const v=value;if(Array.isArray(v.cascade)){const cascade=v.cascade.filter((x)=>typeof x==="string");return cascade.length?{cascade}:{}}return{}}return{}}function resolveChildModel(parentDefinition,relationName){const hasMany=parentDefinition.hasMany??[],hasOne=parentDefinition.hasOne??[],isHasMany=hasMany.some((c)=>relationMatches(c,relationName,!0)),isHasOne=hasOne.some((c)=>relationMatches(c,relationName,!1));if(!isHasMany&&!isHasOne){log.warn(`[orm] soft-delete cascade: '${relationName}' is not a declared hasMany/hasOne relation - skipping`);return null}const className=(isHasMany?hasMany:hasOne).find((c)=>relationMatches(c,relationName,isHasMany))??null;if(!className)return null;const model=globalThis[className];if(!model){log.warn(`[orm] soft-delete cascade: model '${className}' is not yet on globalThis - skipping cascade for '${relationName}'`);return null}return{className,model}}function relationMatches(className,relationName,plural){const base=className.toLowerCase();if(relationName===base)return!0;if(plural&&relationName===`${base}s`)return!0;if(plural&&base.endsWith("y")&&relationName===`${base.slice(0,-1)}ies`)return!0;return!1}async function cascadeChildren(parentClassName,parentId,childModel,foreignKey,action){try{const where=childModel.where;if(typeof where!=="function"){log.warn(`[orm] cascade ${action}: child model has no .where()`);return}if(action==="softDelete"){const childSoftDelete=childModel.softDelete;if(typeof childSoftDelete==="function"){const q=where.call(childModel,foreignKey,parentId),rows=typeof q?.get==="function"?await q.get():Array.isArray(q)?q:[],childPk=childModel.primaryKey||"id";for(const r of rows){const id=r?.[childPk];if(id!=null)await childSoftDelete.call(childModel,id)}return}const q=where.call(childModel,foreignKey,parentId);if(typeof q?.delete==="function"){log.debug(`[orm] cascade soft-delete: child has no softDeletes trait, hard-deleting from ${String(childModel.table??"?")}`);await q.delete();return}}else{const childRestore=childModel.restore;if(typeof childRestore!=="function"){log.debug("[orm] cascade restore: child has no restore() - nothing to do");return}const withTrashed=childModel.withTrashed,q=(typeof withTrashed==="function"?withTrashed.call(childModel):childModel).where?.(foreignKey,parentId),rows=typeof q?.get==="function"?await q.get():[],childPk=childModel.primaryKey||"id";for(const r of rows){const id=r?.[childPk];if(id!=null)await childRestore.call(childModel,id)}}}catch(err){log.warn(`[orm] soft-delete cascade ${action} on ${parentClassName}#${String(parentId)} failed`,{error:err});if(process.env.STACKS_ORM_CASCADE_SWALLOW!=="true")throw err}}export async function cascadeSoftDelete(parentDefinition,options,parentId,action){const cascade=options.cascade;if(!cascade||cascade.length===0)return;const parentFk=`${parentDefinition.name.toLowerCase()}_id`;for(const relationName of cascade){const resolved=resolveChildModel(parentDefinition,relationName);if(!resolved)continue;await cascadeChildren(resolved.className,parentId,resolved.model,parentFk,action)}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/orm",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.256",
5
+ "version": "0.75.258",
6
6
  "description": "The Stacks ORM integration",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -63,31 +63,31 @@
63
63
  "prepublishOnly": "bun run build"
64
64
  },
65
65
  "dependencies": {
66
- "@stacksjs/cache": "0.75.256",
67
- "@stacksjs/cli": "0.75.256",
68
- "@stacksjs/config": "0.75.256",
69
- "@stacksjs/database": "0.75.256",
70
- "@stacksjs/env": "0.75.256",
71
- "@stacksjs/error-handling": "0.75.256",
72
- "@stacksjs/events": "0.75.256",
73
- "@stacksjs/logging": "0.75.256",
74
- "@stacksjs/model-meta": "0.75.256",
75
- "@stacksjs/pagination": "0.75.256",
76
- "@stacksjs/path": "0.75.256",
77
- "@stacksjs/query-builder": "0.75.256",
78
- "@stacksjs/realtime": "0.75.256",
79
- "@stacksjs/router": "0.75.256",
80
- "@stacksjs/storage": "0.75.256",
81
- "@stacksjs/strings": "0.75.256",
66
+ "@stacksjs/cache": "0.75.258",
67
+ "@stacksjs/cli": "0.75.258",
68
+ "@stacksjs/config": "0.75.258",
69
+ "@stacksjs/database": "0.75.258",
70
+ "@stacksjs/env": "0.75.258",
71
+ "@stacksjs/error-handling": "0.75.258",
72
+ "@stacksjs/events": "0.75.258",
73
+ "@stacksjs/logging": "0.75.258",
74
+ "@stacksjs/model-meta": "0.75.258",
75
+ "@stacksjs/pagination": "0.75.258",
76
+ "@stacksjs/path": "0.75.258",
77
+ "@stacksjs/query-builder": "0.75.258",
78
+ "@stacksjs/realtime": "0.75.258",
79
+ "@stacksjs/router": "0.75.258",
80
+ "@stacksjs/storage": "0.75.258",
81
+ "@stacksjs/strings": "0.75.258",
82
82
  "@stacksjs/ts-validation": "^0.5.6",
83
- "@stacksjs/validation": "0.75.256",
83
+ "@stacksjs/validation": "0.75.258",
84
84
  "bun-query-builder": "^0.3.8"
85
85
  },
86
86
  "peerDependencies": {
87
- "@stacksjs/auth": "0.75.256",
88
- "@stacksjs/payments": "0.75.256",
89
- "@stacksjs/queue": "0.75.256",
90
- "@stacksjs/search-engine": "0.75.256",
87
+ "@stacksjs/auth": "0.75.258",
88
+ "@stacksjs/payments": "0.75.258",
89
+ "@stacksjs/queue": "0.75.258",
90
+ "@stacksjs/search-engine": "0.75.258",
91
91
  "stripe": "23.0.0"
92
92
  },
93
93
  "peerDependenciesMeta": {
@@ -108,7 +108,7 @@
108
108
  }
109
109
  },
110
110
  "devDependencies": {
111
- "@stacksjs/types": "0.75.256",
111
+ "@stacksjs/types": "0.75.258",
112
112
  "better-dx": "^0.2.24",
113
113
  "stripe": "23.0.0"
114
114
  }