@stacksjs/orm 0.75.422 → 0.75.423

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auto-crud.js CHANGED
@@ -1 +1 @@
1
- export{isNotNullViolation,isUniqueViolation}from"@stacksjs/error-handling";import{isDateTimeColumnRule,parseSqlDateTime,sqlDateTime,sqlDateTimeInput}from"@stacksjs/database/runtime";import{isNotNullViolation,isUniqueViolation}from"@stacksjs/error-handling";export function mapWriteError(err,modelName,op){const e=err;if(err instanceof Error&&typeof e.status==="number"&&Number.isInteger(e.status)&&e.status>=400&&e.status<600){const body={error:err.message};if(e.details!==void 0)body.details=e.details;return{status:e.status,body}}if(isNotNullViolation(err))return{status:422,body:{error:`${modelName} is missing a required value`}};if(isUniqueViolation(err))return{status:409,body:{error:`${modelName} already exists`}};return{status:500,body:{error:`Failed to ${op} ${modelName}`,detail:String(err)}}}export function toSnakeCase(s){let hasUppercase=!1;for(let index=0;index<s.length;index++){const code=s.charCodeAt(index);if(code>127)return s.replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/([A-Z])([A-Z][a-z])/g,"$1_$2").toLowerCase();if(code>=65&&code<=90)hasUppercase=!0}if(!hasUppercase)return s;let result="";for(let index=0;index<s.length;index++){const code=s.charCodeAt(index);if(code<65||code>90){result+=s[index];continue}const previous=index>0?s.charCodeAt(index-1):0,next=index+1<s.length?s.charCodeAt(index+1):0,followsWord=previous>=97&&previous<=122||previous>=48&&previous<=57,endsAcronym=previous>=65&&previous<=90&&next>=97&&next<=122;if(index>0&&(followsWord||endsAcronym))result+="_";result+=String.fromCharCode(code+32)}return result}export function toSnakeCaseKeys(data){const out={};for(const[k,v]of Object.entries(data))out[toSnakeCase(k)]=v;return out}export function apiBasePath(uri,prefix){const cleanPrefix=String(prefix||"").replace(/^\/+|\/+$/g,""),cleanUri=String(uri).replace(/^\/+/,"");return`/api/${cleanPrefix?`${cleanPrefix}/`:""}${cleanUri}`}export function teamOwnershipField(model){if(!model)return null;const attributes=model.attributes??{};if(Object.prototype.hasOwnProperty.call(attributes,"teamId")||Object.prototype.hasOwnProperty.call(attributes,"team_id"))return"team_id";return model.belongsTo?.some((relation)=>relation==="Team")?"team_id":null}export function userOwnershipField(model){if(!model)return null;const attributes=model.attributes??{};if(Object.prototype.hasOwnProperty.call(attributes,"userId")||Object.prototype.hasOwnProperty.call(attributes,"user_id"))return"user_id";return model.belongsTo?.some((relation)=>relation==="User")?"user_id":null}export function ownershipDeclaredUnscoped(model){return model?.ownership===!1}export function stampOwnership(data,field,value){const ownerKey=Object.keys(data).find((key)=>toSnakeCase(key)===toSnakeCase(field));if(Array.isArray(value)){if(ownerKey===void 0)return{data,error:"Ownership value is required"};if(!value.some((allowed)=>String(allowed)===String(data[ownerKey])))return{data,error:"Ownership value is not available to this caller"};return{data:{...data,[field]:data[ownerKey]}}}if(value===null||value===void 0)return{data,error:"Caller has no ownership identity"};const stamped={...data};for(const key of Object.keys(stamped))if(toSnakeCase(key)===toSnakeCase(field))delete stamped[key];stamped[field]=value;return{data:stamped}}export function getWritableFields(model){if(!model)return[];const fields=Object.entries(model.attributes??{}).filter(([,attribute])=>attribute?.fillable===!0).map(([name])=>name);for(const relation of model.belongsTo??[]){if(typeof relation!=="string"||!relation.trim())continue;const words=toSnakeCase(relation.trim()).split("_").filter(Boolean);if(words.length===0)continue;const relationField=`${words[0]}${words.slice(1).map((word)=>`${word[0]?.toUpperCase()}${word.slice(1)}`).join("")}Id`;fields.push(relationField)}return[...new Set(fields)]}export function filterFillable(body,fillableFields){if(!body||fillableFields.length===0)return{};const result={};let snakeByField;for(const field of fillableFields){if(field in body){result[field]=body[field];continue}snakeByField??=resolveFieldSpellingMap(fillableFields);const snake=snakeByField.get(field);if(snake!==field&&snake in body)result[field]=body[snake]}return result}export function normalizeValidationValue(rule,value){if(rule?.name!=="date"||value instanceof Date||typeof value!=="string")return value;const match=/^(\d{4})-(\d{2})-(\d{2})$/.exec(value);if(!match)return value;const year=Number(match[1]),month=Number(match[2]),day=Number(match[3]),parsed=new Date(Date.UTC(year,month-1,day));if(parsed.getUTCFullYear()!==year||parsed.getUTCMonth()!==month-1||parsed.getUTCDate()!==day)return value;return parsed}const fieldSpellingsByList=new WeakMap,fieldSpellingMaps=new WeakMap;function resolveFieldSpellings(fields){const cached=fieldSpellingsByList.get(fields);if(cached&&cached.length===fields.length){let unchanged=!0;for(let index=0;index<fields.length;index++)if(cached[index]?.field!==fields[index]){unchanged=!1;break}if(unchanged)return cached}const resolved=fields.map((field)=>({field,snake:toSnakeCase(field)}));fieldSpellingsByList.set(fields,resolved);return resolved}function resolveFieldSpellingMap(fields){const spellings=resolveFieldSpellings(fields),cached=fieldSpellingMaps.get(spellings);if(cached)return cached;const resolved=new Map(spellings.map(({field,snake})=>[field,snake]));fieldSpellingMaps.set(spellings,resolved);return resolved}export function dropHiddenInputs(data,hiddenFields){if(!hiddenFields.length)return data;const out={...data};for(const{field,snake}of resolveFieldSpellings(hiddenFields)){delete out[field];if(snake!==field)delete out[snake]}return out}export function stripHidden(record,hiddenFields){if(!record||hiddenFields.length===0)return record;const result={...record};for(const{field,snake}of resolveFieldSpellings(hiddenFields)){delete result[field];if(snake!==field)delete result[snake]}return result}export const SYSTEM_COLUMNS=["id","uuid","created_at","updated_at","deleted_at"];export function buildReadColumnMap(attributes,hiddenFields){const map=new Map;for(const name of[...Object.keys(attributes??{}),...SYSTEM_COLUMNS]){const column=toSnakeCase(name);if(!/^\w+$/.test(column))continue;map.set(name,column);map.set(column,column)}for(const f of hiddenFields){map.delete(f);map.delete(toSnakeCase(f))}return map}export function applySorting(query,sortParam,columns){if(!sortParam)return query;let q=query;for(const rawToken of String(sortParam).split(",")){const tok=rawToken.trim();if(!tok)continue;const desc=tok.startsWith("-"),requested=desc?tok.slice(1):tok;if(!/^\w+$/.test(requested))continue;const column=columns.get(requested);if(!column)continue;q=q.orderBy(column,desc?"desc":"asc")}return q}export const AUTO_CRUD_CASTERS={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)=>v==null?null:typeof v==="string"?safeJSON(v):v,set:(v)=>v==null?null: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)=>v==null?[]:Array.isArray(v)?v:typeof v==="string"?safeJSONOrEmpty(v):[],set:(v)=>v==null?null:Array.isArray(v)?JSON.stringify(v):v}};const autoCrudCastsByDefinition=new WeakMap;function resolveAutoCrudCasts(casts,attrs){const cached=autoCrudCastsByDefinition.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=[];for(const attr of attrs){const castDef=casts[attr],caster=typeof castDef==="string"?AUTO_CRUD_CASTERS[castDef]:castDef;resolved.push({attr,snake:toSnakeCase(attr),definition:castDef,caster})}autoCrudCastsByDefinition.set(casts,resolved);return resolved}function safeJSON(s){try{return JSON.parse(s)}catch{return s}}function safeJSONOrEmpty(_s){try{return JSON.parse(_s)}catch{return[]}}export function applyCasts(record,casts,direction){if(!record||typeof record!=="object"||!casts)return record;const attrs=Object.keys(casts);if(attrs.length===0)return record;const resolvedCasts=resolveAutoCrudCasts(casts,attrs);if(resolvedCasts.length===0)return record;const out={...record};for(const{attr,caster,snake}of resolvedCasts){if(!caster)continue;const cast=caster[direction];if(typeof cast!=="function")continue;if(Object.prototype.hasOwnProperty.call(out,attr))out[attr]=cast.call(caster,out[attr]);if(snake!==attr&&Object.prototype.hasOwnProperty.call(out,snake))out[snake]=cast.call(caster,out[snake])}return out}const writeValidatorsByAttributes=new WeakMap;function resolveWriteValidators(attributes){const cached=writeValidatorsByAttributes.get(attributes);if(cached)return cached;const validators=[];for(const[field,definition]of Object.entries(attributes)){const rule=definition?.validation?.rule;if(!rule||typeof rule.validate!=="function")continue;validators.push({definition,field,hasDefault:definition!==null&&typeof definition==="object"&&Object.prototype.hasOwnProperty.call(definition,"default"),rule})}writeValidatorsByAttributes.set(attributes,validators);return validators}export function normalizeTimestampWrites(data,model){if(!data||typeof data!=="object"||Array.isArray(data))return data;let result=data;for(const key of dateTimeWriteKeys(model)){if(!Object.prototype.hasOwnProperty.call(data,key))continue;const converted=sqlDateTimeInput(data[key]);if(converted!==data[key]){if(result===data)result={...data};result[key]=converted}}return result}export function dateTimeWriteKeys(model){const keys=new Set,attributes=model?.attributes??{},declared=new Set(Object.keys(attributes).map(toSnakeCase)),traits=model?.traits??{},traitColumns=[];if((traits.useTimestamps??traits.timestampable)!==!1)traitColumns.push("created_at","updated_at");if(traits.useSoftDeletes??traits.softDeletable)traitColumns.push("deleted_at");for(const column of traitColumns)if(!declared.has(column))keys.add(column);for(const{field,rule}of resolveWriteValidators(attributes))if(isDateTimeColumnRule(rule)){keys.add(field);keys.add(toSnakeCase(field))}return keys}export function hasWriteValidators(model){const attributes=model?.attributes;if(!attributes||typeof attributes!=="object")return!1;for(const definition of Object.values(attributes)){const rule=definition?.validation?.rule;if(rule&&typeof rule.validate==="function")return!0}return!1}export function validateWriteBody(data,model,hook){const attributes=model?.attributes;if(!attributes||typeof attributes!=="object")return{valid:!0};const validators=resolveWriteValidators(attributes);if(validators.length===0)return{valid:!0};const errors={};let hasErrors=!1;for(const validator of validators){const{definition,field,hasDefault,rule}=validator,column=toSnakeCase(field),key=Object.prototype.hasOwnProperty.call(data,field)?field:column,present=Object.prototype.hasOwnProperty.call(data,key);if(!present&&hook==="updating")continue;const raw=present?data[key]:hasDefault?definition.default:void 0,value=normalizeValidationValue(rule,raw),result=rule.validate(value);if(!result?.valid&&Array.isArray(result?.errors)&&result.errors.length>0){hasErrors=!0;errors[field]=result.errors.map((e)=>definition.validation?.message?.[e?.code]??e?.message??"invalid")}}return hasErrors?{valid:!1,errors}:{valid:!0}}export function routeShapeKey(method,path){return`${method}\x00${routeShape(path)}`}export function indexRouteShapes(routes){const index=new Map;for(const route of routes){const key=routeShapeKey(String(route.method??""),String(route.path??""));if(!index.has(key))index.set(key,route)}return index}export function findShadowingRoute(routes,method,path){const shape=routeShape(path);return routes.find((r)=>r.method===method&&routeShape(String(r.path??""))===shape)}export function routeShape(path){return path.replace(/\{[^}]*\}/g,"{}").replace(/:[^/]+/g,"{}")}function middlewareList(raw){if(Array.isArray(raw))return raw.filter((m)=>typeof m==="string"&&m.length>0);return typeof raw==="string"&&raw?[raw]:[]}export function resolveApiMiddleware(useApi,dashboard){const declared=typeof useApi==="object"&&useApi!==null&&"middleware"in useApi,raw=useApi?.middleware,role=dashboardRoleMiddleware(dashboard);if(!declared)return{read:withDashboardRole(["auth"],role),write:withDashboardRole(["auth"],role),declared:!1};if(raw&&typeof raw==="object"&&!Array.isArray(raw)){const split=raw;return{read:withDashboardRole("read"in split?middlewareList(split.read):["auth"],role),write:withDashboardRole("write"in split?middlewareList(split.write):["auth"],role),declared:!0}}const list=middlewareList(raw);return{read:withDashboardRole(list,role),write:withDashboardRole(list,role),declared:!0}}export function dashboardRoleMiddleware(dashboard){if(!dashboard||typeof dashboard!=="object")return null;const options=dashboard;if(options.enforce===!1)return null;if(!Array.isArray(options.roles))return null;const roles=options.roles.filter((role)=>typeof role==="string").map((role)=>role.trim()).filter((role)=>role.length>0);return roles.length>0?`role:${roles.join(",")}`:null}export const API_ABILITIES=["index","show","store","update","destroy"];const ABILITY_SIDE={index:"read",show:"read",store:"write",update:"write",destroy:"write"};export function resolveAbilityMiddleware(useApi,dashboard){const{read,write,declared}=resolveApiMiddleware(useApi,dashboard),role=dashboardRoleMiddleware(dashboard),raw=useApi?.middleware,declaredPerAbility=raw&&typeof raw==="object"&&!Array.isArray(raw)?raw:void 0,abilities={};for(const ability of API_ABILITIES)abilities[ability]=declaredPerAbility&&ability in declaredPerAbility?withDashboardRole(middlewareList(declaredPerAbility[ability]),role):ABILITY_SIDE[ability]==="read"?read:write;return{abilities,read,write,declared}}function withDashboardRole(list,role){if(!role||list.length===0)return list;if(list.some((entry)=>entry==="role"||entry.startsWith("role:")))return list;return[...list,role]}export function describeContradictoryRoleGates(entries){if(entries.length===0)return null;const named=[...entries].sort((a,b)=>a.model.localeCompare(b.model)).map((entry)=>`${entry.model} (${[...entry.abilities].join(", ")})`).join(", ");return`[orm] ${entries.length} model(s) role-gate their sidebar row and publish part of the matching API to everyone, because \`useApi.middleware\` declares those abilities empty - ${named}. Drop the empty list to let \`dashboard.roles\` gate them, or set \`dashboard.enforce: false\` to keep the row hidden and the API public.`}export const POLICY_ABILITY={index:"viewAny",show:"view",store:"create",update:"update",destroy:"delete"};export async function policyDecision(modelName,ability,deps,subject){let gate;try{gate=await deps.gate()}catch{return null}if(!gate||!gate.hasPolicy(modelName))return null;try{const user=await deps.user(),verdict=subject===void 0?await gate.inspectFor(modelName,ability,user):await gate.inspectFor(modelName,ability,user,subject);if(verdict&&!verdict.isAllowed)return{status:403,message:verdict.message||"This action is unauthorized."};return null}catch{return{status:403,message:"This action is unauthorized."}}}export const INDEX_DEFAULT_PER_PAGE=15,INDEX_MAX_PER_PAGE=100;export function resolveIndexPageArgs(params){const pageRaw=Number.parseInt(params.get("page")||String(1),10),page=Number.isFinite(pageRaw)?Math.max(1,pageRaw):1,perPageRaw=Number.parseInt(params.get("per_page")||String(INDEX_DEFAULT_PER_PAGE),10),perPage=Math.min(Number.isFinite(perPageRaw)?Math.max(1,perPageRaw):INDEX_DEFAULT_PER_PAGE,INDEX_MAX_PER_PAGE);return{page,perPage,offset:(page-1)*perPage}}function pageUrl(url,page){const out=new URL(url.toString());out.searchParams.set("page",String(page));return`${out.pathname}${out.search}`}export function buildIndexMeta(url,page,perPage,rowCount,hasMore,total){const offset=(page-1)*perPage,empty=rowCount===0,meta={page,per_page:perPage,from:empty?null:offset+1,to:empty?null:offset+rowCount,has_more_pages:hasMore,prev_page_url:page>1?pageUrl(url,page-1):null,next_page_url:hasMore?pageUrl(url,page+1):null};if(total!==void 0&&!Number.isNaN(total)){const lastPage=Math.max(1,Math.ceil(total/perPage));meta.total=total;meta.last_page=lastPage;meta.first_page_url=pageUrl(url,1);meta.last_page_url=pageUrl(url,lastPage)}return meta}export function buildIndexPaginator(url,page,perPage,rowCount,hasMore,total){const{page:currentPage,...rest}=buildIndexMeta(url,page,perPage,rowCount,hasMore,total);return{current_page:currentPage,...rest}}export function resolveRowScopingPolicy(configured,envOverride){return(envOverride??configured??"").toString().trim().toLowerCase()==="warn"?"warn":"deny"}export function describeUnscopedMutatingModels(modelNames,policy){if(modelNames.length===0)return null;const names=[...modelNames].sort().join(", ");if(policy==="deny")return`[orm] security.api.rowScoping is 'deny': NOT registering store/update/destroy for ${modelNames.length} model(s) `+`with no row-level scoping \u2014 ${names}. Give a model an \`ownership\` config or a \`team_id\` column to restore its writes.`;return`[orm] ${modelNames.length} model(s) publish mutating routes with no row-level scoping, so an authenticated caller can write any row \u2014 `+`${names}. Scope one with an \`ownership\` config or a \`team_id\` column; intended for public catalog tables. Set \`security.api.rowScoping: 'deny'\` (config/security.ts) to stop generating these routes instead.`}
1
+ export{isNotNullViolation,isUniqueViolation}from"@stacksjs/error-handling";import{isDateTimeColumnRule,parseSqlDateTime,sqlDateTime,sqlDateTimeInput}from"@stacksjs/database/runtime";import{isNotNullViolation,isUniqueViolation}from"@stacksjs/error-handling";import{fieldErrorMessages}from"@stacksjs/validation/messages";export function mapWriteError(err,modelName,op){const e=err;if(err instanceof Error&&typeof e.status==="number"&&Number.isInteger(e.status)&&e.status>=400&&e.status<600){const body={error:err.message};if(e.details!==void 0)body.details=e.details;return{status:e.status,body}}if(isNotNullViolation(err))return{status:422,body:{error:`${modelName} is missing a required value`}};if(isUniqueViolation(err))return{status:409,body:{error:`${modelName} already exists`}};return{status:500,body:{error:`Failed to ${op} ${modelName}`,detail:String(err)}}}export function toSnakeCase(s){let hasUppercase=!1;for(let index=0;index<s.length;index++){const code=s.charCodeAt(index);if(code>127)return s.replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/([A-Z])([A-Z][a-z])/g,"$1_$2").toLowerCase();if(code>=65&&code<=90)hasUppercase=!0}if(!hasUppercase)return s;let result="";for(let index=0;index<s.length;index++){const code=s.charCodeAt(index);if(code<65||code>90){result+=s[index];continue}const previous=index>0?s.charCodeAt(index-1):0,next=index+1<s.length?s.charCodeAt(index+1):0,followsWord=previous>=97&&previous<=122||previous>=48&&previous<=57,endsAcronym=previous>=65&&previous<=90&&next>=97&&next<=122;if(index>0&&(followsWord||endsAcronym))result+="_";result+=String.fromCharCode(code+32)}return result}export function toSnakeCaseKeys(data){const out={};for(const[k,v]of Object.entries(data))out[toSnakeCase(k)]=v;return out}export function apiBasePath(uri,prefix){const cleanPrefix=String(prefix||"").replace(/^\/+|\/+$/g,""),cleanUri=String(uri).replace(/^\/+/,"");return`/api/${cleanPrefix?`${cleanPrefix}/`:""}${cleanUri}`}export function teamOwnershipField(model){if(!model)return null;const attributes=model.attributes??{};if(Object.prototype.hasOwnProperty.call(attributes,"teamId")||Object.prototype.hasOwnProperty.call(attributes,"team_id"))return"team_id";return model.belongsTo?.some((relation)=>relation==="Team")?"team_id":null}export function userOwnershipField(model){if(!model)return null;const attributes=model.attributes??{};if(Object.prototype.hasOwnProperty.call(attributes,"userId")||Object.prototype.hasOwnProperty.call(attributes,"user_id"))return"user_id";return model.belongsTo?.some((relation)=>relation==="User")?"user_id":null}export function ownershipDeclaredUnscoped(model){return model?.ownership===!1}export function stampOwnership(data,field,value){const ownerKey=Object.keys(data).find((key)=>toSnakeCase(key)===toSnakeCase(field));if(Array.isArray(value)){if(ownerKey===void 0)return{data,error:"Ownership value is required"};if(!value.some((allowed)=>String(allowed)===String(data[ownerKey])))return{data,error:"Ownership value is not available to this caller"};return{data:{...data,[field]:data[ownerKey]}}}if(value===null||value===void 0)return{data,error:"Caller has no ownership identity"};const stamped={...data};for(const key of Object.keys(stamped))if(toSnakeCase(key)===toSnakeCase(field))delete stamped[key];stamped[field]=value;return{data:stamped}}export function getWritableFields(model){if(!model)return[];const fields=Object.entries(model.attributes??{}).filter(([,attribute])=>attribute?.fillable===!0).map(([name])=>name);for(const relation of model.belongsTo??[]){if(typeof relation!=="string"||!relation.trim())continue;const words=toSnakeCase(relation.trim()).split("_").filter(Boolean);if(words.length===0)continue;const relationField=`${words[0]}${words.slice(1).map((word)=>`${word[0]?.toUpperCase()}${word.slice(1)}`).join("")}Id`;fields.push(relationField)}return[...new Set(fields)]}export function filterFillable(body,fillableFields){if(!body||fillableFields.length===0)return{};const result={};let snakeByField;for(const field of fillableFields){if(field in body){result[field]=body[field];continue}snakeByField??=resolveFieldSpellingMap(fillableFields);const snake=snakeByField.get(field);if(snake!==field&&snake in body)result[field]=body[snake]}return result}export function normalizeValidationValue(rule,value){if(rule?.name!=="date"||value instanceof Date||typeof value!=="string")return value;const match=/^(\d{4})-(\d{2})-(\d{2})$/.exec(value);if(!match)return value;const year=Number(match[1]),month=Number(match[2]),day=Number(match[3]),parsed=new Date(Date.UTC(year,month-1,day));if(parsed.getUTCFullYear()!==year||parsed.getUTCMonth()!==month-1||parsed.getUTCDate()!==day)return value;return parsed}const fieldSpellingsByList=new WeakMap,fieldSpellingMaps=new WeakMap;function resolveFieldSpellings(fields){const cached=fieldSpellingsByList.get(fields);if(cached&&cached.length===fields.length){let unchanged=!0;for(let index=0;index<fields.length;index++)if(cached[index]?.field!==fields[index]){unchanged=!1;break}if(unchanged)return cached}const resolved=fields.map((field)=>({field,snake:toSnakeCase(field)}));fieldSpellingsByList.set(fields,resolved);return resolved}function resolveFieldSpellingMap(fields){const spellings=resolveFieldSpellings(fields),cached=fieldSpellingMaps.get(spellings);if(cached)return cached;const resolved=new Map(spellings.map(({field,snake})=>[field,snake]));fieldSpellingMaps.set(spellings,resolved);return resolved}export function dropHiddenInputs(data,hiddenFields){if(!hiddenFields.length)return data;const out={...data};for(const{field,snake}of resolveFieldSpellings(hiddenFields)){delete out[field];if(snake!==field)delete out[snake]}return out}export function stripHidden(record,hiddenFields){if(!record||hiddenFields.length===0)return record;const result={...record};for(const{field,snake}of resolveFieldSpellings(hiddenFields)){delete result[field];if(snake!==field)delete result[snake]}return result}export const SYSTEM_COLUMNS=["id","uuid","created_at","updated_at","deleted_at"];export function buildReadColumnMap(attributes,hiddenFields){const map=new Map;for(const name of[...Object.keys(attributes??{}),...SYSTEM_COLUMNS]){const column=toSnakeCase(name);if(!/^\w+$/.test(column))continue;map.set(name,column);map.set(column,column)}for(const f of hiddenFields){map.delete(f);map.delete(toSnakeCase(f))}return map}export function applySorting(query,sortParam,columns){if(!sortParam)return query;let q=query;for(const rawToken of String(sortParam).split(",")){const tok=rawToken.trim();if(!tok)continue;const desc=tok.startsWith("-"),requested=desc?tok.slice(1):tok;if(!/^\w+$/.test(requested))continue;const column=columns.get(requested);if(!column)continue;q=q.orderBy(column,desc?"desc":"asc")}return q}export const AUTO_CRUD_CASTERS={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)=>v==null?null:typeof v==="string"?safeJSON(v):v,set:(v)=>v==null?null: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)=>v==null?[]:Array.isArray(v)?v:typeof v==="string"?safeJSONOrEmpty(v):[],set:(v)=>v==null?null:Array.isArray(v)?JSON.stringify(v):v}};const autoCrudCastsByDefinition=new WeakMap;function resolveAutoCrudCasts(casts,attrs){const cached=autoCrudCastsByDefinition.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=[];for(const attr of attrs){const castDef=casts[attr],caster=typeof castDef==="string"?AUTO_CRUD_CASTERS[castDef]:castDef;resolved.push({attr,snake:toSnakeCase(attr),definition:castDef,caster})}autoCrudCastsByDefinition.set(casts,resolved);return resolved}function safeJSON(s){try{return JSON.parse(s)}catch{return s}}function safeJSONOrEmpty(_s){try{return JSON.parse(_s)}catch{return[]}}export function applyCasts(record,casts,direction){if(!record||typeof record!=="object"||!casts)return record;const attrs=Object.keys(casts);if(attrs.length===0)return record;const resolvedCasts=resolveAutoCrudCasts(casts,attrs);if(resolvedCasts.length===0)return record;const out={...record};for(const{attr,caster,snake}of resolvedCasts){if(!caster)continue;const cast=caster[direction];if(typeof cast!=="function")continue;if(Object.prototype.hasOwnProperty.call(out,attr))out[attr]=cast.call(caster,out[attr]);if(snake!==attr&&Object.prototype.hasOwnProperty.call(out,snake))out[snake]=cast.call(caster,out[snake])}return out}const writeValidatorsByAttributes=new WeakMap;function resolveWriteValidators(attributes){const cached=writeValidatorsByAttributes.get(attributes);if(cached)return cached;const validators=[];for(const[field,definition]of Object.entries(attributes)){const rule=definition?.validation?.rule;if(!rule||typeof rule.validate!=="function")continue;validators.push({definition,field,hasDefault:definition!==null&&typeof definition==="object"&&Object.prototype.hasOwnProperty.call(definition,"default"),rule})}writeValidatorsByAttributes.set(attributes,validators);return validators}export function normalizeTimestampWrites(data,model){if(!data||typeof data!=="object"||Array.isArray(data))return data;let result=data;for(const key of dateTimeWriteKeys(model)){if(!Object.prototype.hasOwnProperty.call(data,key))continue;const converted=sqlDateTimeInput(data[key]);if(converted!==data[key]){if(result===data)result={...data};result[key]=converted}}return result}export function dateTimeWriteKeys(model){const keys=new Set,attributes=model?.attributes??{},declared=new Set(Object.keys(attributes).map(toSnakeCase)),traits=model?.traits??{},traitColumns=[];if((traits.useTimestamps??traits.timestampable)!==!1)traitColumns.push("created_at","updated_at");if(traits.useSoftDeletes??traits.softDeletable)traitColumns.push("deleted_at");for(const column of traitColumns)if(!declared.has(column))keys.add(column);for(const{field,rule}of resolveWriteValidators(attributes))if(isDateTimeColumnRule(rule)){keys.add(field);keys.add(toSnakeCase(field))}return keys}export function hasWriteValidators(model){const attributes=model?.attributes;if(!attributes||typeof attributes!=="object")return!1;for(const definition of Object.values(attributes)){const rule=definition?.validation?.rule;if(rule&&typeof rule.validate==="function")return!0}return!1}export function validateWriteBody(data,model,hook){const attributes=model?.attributes;if(!attributes||typeof attributes!=="object")return{valid:!0};const validators=resolveWriteValidators(attributes);if(validators.length===0)return{valid:!0};const errors={};let hasErrors=!1;for(const validator of validators){const{definition,field,hasDefault,rule}=validator,column=toSnakeCase(field),key=Object.prototype.hasOwnProperty.call(data,field)?field:column,present=Object.prototype.hasOwnProperty.call(data,key);if(!present&&hook==="updating")continue;const raw=present?data[key]:hasDefault?definition.default:void 0,value=normalizeValidationValue(rule,raw),result=rule.validate(value);if(!result?.valid&&Array.isArray(result?.errors)&&result.errors.length>0){hasErrors=!0;errors[field]=fieldErrorMessages(rule,value,result.errors.map((e)=>({message:typeof e?.message==="string"?e.message:"invalid"})),definition.validation?.message,field)}}return hasErrors?{valid:!1,errors}:{valid:!0}}export function routeShapeKey(method,path){return`${method}\x00${routeShape(path)}`}export function indexRouteShapes(routes){const index=new Map;for(const route of routes){const key=routeShapeKey(String(route.method??""),String(route.path??""));if(!index.has(key))index.set(key,route)}return index}export function findShadowingRoute(routes,method,path){const shape=routeShape(path);return routes.find((r)=>r.method===method&&routeShape(String(r.path??""))===shape)}export function routeShape(path){return path.replace(/\{[^}]*\}/g,"{}").replace(/:[^/]+/g,"{}")}function middlewareList(raw){if(Array.isArray(raw))return raw.filter((m)=>typeof m==="string"&&m.length>0);return typeof raw==="string"&&raw?[raw]:[]}export function resolveApiMiddleware(useApi,dashboard){const declared=typeof useApi==="object"&&useApi!==null&&"middleware"in useApi,raw=useApi?.middleware,role=dashboardRoleMiddleware(dashboard);if(!declared)return{read:withDashboardRole(["auth"],role),write:withDashboardRole(["auth"],role),declared:!1};if(raw&&typeof raw==="object"&&!Array.isArray(raw)){const split=raw;return{read:withDashboardRole("read"in split?middlewareList(split.read):["auth"],role),write:withDashboardRole("write"in split?middlewareList(split.write):["auth"],role),declared:!0}}const list=middlewareList(raw);return{read:withDashboardRole(list,role),write:withDashboardRole(list,role),declared:!0}}export function dashboardRoleMiddleware(dashboard){if(!dashboard||typeof dashboard!=="object")return null;const options=dashboard;if(options.enforce===!1)return null;if(!Array.isArray(options.roles))return null;const roles=options.roles.filter((role)=>typeof role==="string").map((role)=>role.trim()).filter((role)=>role.length>0);return roles.length>0?`role:${roles.join(",")}`:null}export const API_ABILITIES=["index","show","store","update","destroy"];const ABILITY_SIDE={index:"read",show:"read",store:"write",update:"write",destroy:"write"};export function resolveAbilityMiddleware(useApi,dashboard){const{read,write,declared}=resolveApiMiddleware(useApi,dashboard),role=dashboardRoleMiddleware(dashboard),raw=useApi?.middleware,declaredPerAbility=raw&&typeof raw==="object"&&!Array.isArray(raw)?raw:void 0,abilities={};for(const ability of API_ABILITIES)abilities[ability]=declaredPerAbility&&ability in declaredPerAbility?withDashboardRole(middlewareList(declaredPerAbility[ability]),role):ABILITY_SIDE[ability]==="read"?read:write;return{abilities,read,write,declared}}function withDashboardRole(list,role){if(!role||list.length===0)return list;if(list.some((entry)=>entry==="role"||entry.startsWith("role:")))return list;return[...list,role]}export function describeContradictoryRoleGates(entries){if(entries.length===0)return null;const named=[...entries].sort((a,b)=>a.model.localeCompare(b.model)).map((entry)=>`${entry.model} (${[...entry.abilities].join(", ")})`).join(", ");return`[orm] ${entries.length} model(s) role-gate their sidebar row and publish part of the matching API to everyone, because \`useApi.middleware\` declares those abilities empty - ${named}. Drop the empty list to let \`dashboard.roles\` gate them, or set \`dashboard.enforce: false\` to keep the row hidden and the API public.`}export const POLICY_ABILITY={index:"viewAny",show:"view",store:"create",update:"update",destroy:"delete"};export async function policyDecision(modelName,ability,deps,subject){let gate;try{gate=await deps.gate()}catch{return null}if(!gate||!gate.hasPolicy(modelName))return null;try{const user=await deps.user(),verdict=subject===void 0?await gate.inspectFor(modelName,ability,user):await gate.inspectFor(modelName,ability,user,subject);if(verdict&&!verdict.isAllowed)return{status:403,message:verdict.message||"This action is unauthorized."};return null}catch{return{status:403,message:"This action is unauthorized."}}}export const INDEX_DEFAULT_PER_PAGE=15,INDEX_MAX_PER_PAGE=100;export function resolveIndexPageArgs(params){const pageRaw=Number.parseInt(params.get("page")||String(1),10),page=Number.isFinite(pageRaw)?Math.max(1,pageRaw):1,perPageRaw=Number.parseInt(params.get("per_page")||String(INDEX_DEFAULT_PER_PAGE),10),perPage=Math.min(Number.isFinite(perPageRaw)?Math.max(1,perPageRaw):INDEX_DEFAULT_PER_PAGE,INDEX_MAX_PER_PAGE);return{page,perPage,offset:(page-1)*perPage}}function pageUrl(url,page){const out=new URL(url.toString());out.searchParams.set("page",String(page));return`${out.pathname}${out.search}`}export function buildIndexMeta(url,page,perPage,rowCount,hasMore,total){const offset=(page-1)*perPage,empty=rowCount===0,meta={page,per_page:perPage,from:empty?null:offset+1,to:empty?null:offset+rowCount,has_more_pages:hasMore,prev_page_url:page>1?pageUrl(url,page-1):null,next_page_url:hasMore?pageUrl(url,page+1):null};if(total!==void 0&&!Number.isNaN(total)){const lastPage=Math.max(1,Math.ceil(total/perPage));meta.total=total;meta.last_page=lastPage;meta.first_page_url=pageUrl(url,1);meta.last_page_url=pageUrl(url,lastPage)}return meta}export function buildIndexPaginator(url,page,perPage,rowCount,hasMore,total){const{page:currentPage,...rest}=buildIndexMeta(url,page,perPage,rowCount,hasMore,total);return{current_page:currentPage,...rest}}export function resolveRowScopingPolicy(configured,envOverride){return(envOverride??configured??"").toString().trim().toLowerCase()==="warn"?"warn":"deny"}export function describeUnscopedMutatingModels(modelNames,policy){if(modelNames.length===0)return null;const names=[...modelNames].sort().join(", ");if(policy==="deny")return`[orm] security.api.rowScoping is 'deny': NOT registering store/update/destroy for ${modelNames.length} model(s) `+`with no row-level scoping \u2014 ${names}. Give a model an \`ownership\` config or a \`team_id\` column to restore its writes.`;return`[orm] ${modelNames.length} model(s) publish mutating routes with no row-level scoping, so an authenticated caller can write any row \u2014 `+`${names}. Scope one with an \`ownership\` config or a \`team_id\` column; intended for public catalog tables. Set \`security.api.rowScoping: 'deny'\` (config/security.ts) to stop generating these routes instead.`}
@@ -216,7 +216,7 @@ export default defineModel({
216
216
  validation: {
217
217
  rule: schema.enum(['published', 'draft', 'archived']),
218
218
  message: {
219
- oneOf: 'Status must be either published, draft, or archived',
219
+ enum: 'Status must be either published, draft, or archived',
220
220
  },
221
221
  },
222
222
  factory: faker => faker.helpers.arrayElement(['published', 'draft', 'archived']),
@@ -71,7 +71,7 @@ export default defineModel({
71
71
  validation: {
72
72
  rule: schema.number(),
73
73
  message: {
74
- string: 'status must be a number',
74
+ number: 'status must be a number',
75
75
  },
76
76
  },
77
77
  factory: faker => faker.number.int({ min: 400, max: 500 }),
@@ -53,7 +53,7 @@ export default defineModel({
53
53
  // (stacksjs/stacks#2996).
54
54
  rule: schema.unix().required(),
55
55
  message: {
56
- date: 'Timestamp must be a valid date',
56
+ unix: 'Timestamp must be a valid Unix timestamp',
57
57
  },
58
58
  },
59
59
  factory: faker => faker.date.recent().getTime(),
@@ -50,7 +50,7 @@ export default defineModel({
50
50
  validation: {
51
51
  rule: schema.string().required().max(50),
52
52
  message: {
53
- number: 'brand must be a number',
53
+ string: 'brand must be a string',
54
54
  required: 'brand is required',
55
55
  },
56
56
  },
@@ -63,7 +63,7 @@ export default defineModel({
63
63
  validation: {
64
64
  rule: schema.number().required(),
65
65
  message: {
66
- string: 'exp_month must be a number',
66
+ number: 'exp_month must be a number',
67
67
  required: 'exp_month is required',
68
68
  },
69
69
  },
@@ -76,7 +76,7 @@ export default defineModel({
76
76
  validation: {
77
77
  rule: schema.number().required(),
78
78
  message: {
79
- string: 'exp_year must be a number',
79
+ number: 'exp_year must be a number',
80
80
  required: 'exp_year is required',
81
81
  },
82
82
  },
@@ -55,7 +55,7 @@ export default defineModel({
55
55
  validation: {
56
56
  rule: schema.number().required(),
57
57
  message: {
58
- string: 'expires must be a string',
58
+ number: 'unit_price must be a number',
59
59
  required: 'expires is required',
60
60
  },
61
61
  },
@@ -70,7 +70,7 @@ export default defineModel({
70
70
  validation: {
71
71
  rule: schema.number().required(),
72
72
  message: {
73
- string: 'unit_price must be a number',
73
+ number: 'unit_price must be a number',
74
74
  required: 'unit_price is required',
75
75
  },
76
76
  },
@@ -100,7 +100,7 @@ export default defineModel({
100
100
  validation: {
101
101
  rule: schema.unix(),
102
102
  message: {
103
- invalid: 'Invalid timestamp format',
103
+ unix: 'Invalid timestamp format',
104
104
  },
105
105
  },
106
106
  factory: faker => faker.date.recent().getTime(),
@@ -79,7 +79,7 @@ export default defineModel({
79
79
  validation: {
80
80
  rule: schema.timestampTz().required(),
81
81
  message: {
82
- invalid: 'Invalid date format',
82
+ timestampTz: 'Invalid date format',
83
83
  },
84
84
  },
85
85
  factory: faker => faker.date.recent().toISOString(),
@@ -33,7 +33,7 @@ export default defineModel({
33
33
  validation: {
34
34
  rule: schema.enum(['disconnection', 'error', 'success']),
35
35
  message: {
36
- in: 'Type must be one of: disconnection, error, success',
36
+ enum: 'Type must be one of: disconnection, error, success',
37
37
  },
38
38
  },
39
39
  factory: faker => faker.helpers.arrayElement(['success', 'success', 'success', 'disconnection', 'error']),
@@ -77,7 +77,7 @@ export default defineModel({
77
77
  // and MySQL (stacksjs/stacks#2996).
78
78
  rule: schema.unix(),
79
79
  message: {
80
- date: 'Time must be a valid timestamp',
80
+ unix: 'Time must be a valid timestamp',
81
81
  },
82
82
  },
83
83
  factory: faker => faker.date.recent().getTime(),
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.422",
5
+ "version": "0.75.423",
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.422",
67
- "@stacksjs/cli": "0.75.422",
68
- "@stacksjs/config": "0.75.422",
69
- "@stacksjs/database": "0.75.422",
70
- "@stacksjs/env": "0.75.422",
71
- "@stacksjs/error-handling": "0.75.422",
72
- "@stacksjs/events": "0.75.422",
73
- "@stacksjs/logging": "0.75.422",
74
- "@stacksjs/model-meta": "0.75.422",
75
- "@stacksjs/pagination": "0.75.422",
76
- "@stacksjs/path": "0.75.422",
77
- "@stacksjs/query-builder": "0.75.422",
78
- "@stacksjs/realtime": "0.75.422",
79
- "@stacksjs/router": "0.75.422",
80
- "@stacksjs/storage": "0.75.422",
81
- "@stacksjs/strings": "0.75.422",
66
+ "@stacksjs/cache": "0.75.423",
67
+ "@stacksjs/cli": "0.75.423",
68
+ "@stacksjs/config": "0.75.423",
69
+ "@stacksjs/database": "0.75.423",
70
+ "@stacksjs/env": "0.75.423",
71
+ "@stacksjs/error-handling": "0.75.423",
72
+ "@stacksjs/events": "0.75.423",
73
+ "@stacksjs/logging": "0.75.423",
74
+ "@stacksjs/model-meta": "0.75.423",
75
+ "@stacksjs/pagination": "0.75.423",
76
+ "@stacksjs/path": "0.75.423",
77
+ "@stacksjs/query-builder": "0.75.423",
78
+ "@stacksjs/realtime": "0.75.423",
79
+ "@stacksjs/router": "0.75.423",
80
+ "@stacksjs/storage": "0.75.423",
81
+ "@stacksjs/strings": "0.75.423",
82
82
  "@stacksjs/ts-validation": "^0.5.6",
83
- "@stacksjs/validation": "0.75.422",
83
+ "@stacksjs/validation": "0.75.423",
84
84
  "bun-query-builder": "^0.3.32"
85
85
  },
86
86
  "peerDependencies": {
87
- "@stacksjs/auth": "0.75.422",
88
- "@stacksjs/payments": "0.75.422",
89
- "@stacksjs/queue": "0.75.422",
90
- "@stacksjs/search-engine": "0.75.422",
87
+ "@stacksjs/auth": "0.75.423",
88
+ "@stacksjs/payments": "0.75.423",
89
+ "@stacksjs/queue": "0.75.423",
90
+ "@stacksjs/search-engine": "0.75.423",
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.422",
111
+ "@stacksjs/types": "0.75.423",
112
112
  "better-dx": "^0.2.24",
113
113
  "stripe": "23.0.0"
114
114
  }