@stacksjs/orm 0.74.1 → 0.74.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/auto-crud.d.ts +30 -0
  2. package/dist/generate-database-schema.d.ts +1 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +9 -9
  5. package/dist/migration-tables.d.ts +18 -0
  6. package/dist/models/AnalyticsEvent.ts +5 -0
  7. package/dist/models/Content/Menu.ts +5 -1
  8. package/dist/models/Content/MenuItem.ts +6 -1
  9. package/dist/models/Content/Page.ts +5 -1
  10. package/dist/models/Content/Post.ts +5 -1
  11. package/dist/models/Content/Redirect.ts +5 -1
  12. package/dist/models/EmailIdempotency.ts +5 -0
  13. package/dist/models/EmailList.ts +6 -0
  14. package/dist/models/EmailSuppression.ts +5 -0
  15. package/dist/models/EmailWebhookEvent.ts +5 -0
  16. package/dist/models/Forms/Form.ts +5 -1
  17. package/dist/models/Forms/FormField.ts +6 -1
  18. package/dist/models/MailPreference.ts +5 -0
  19. package/dist/models/Request.ts +5 -0
  20. package/dist/models/SiteDomain.ts +5 -1
  21. package/dist/models/Tag.ts +6 -0
  22. package/dist/models/Team.ts +6 -1
  23. package/dist/models/User.ts +6 -1
  24. package/dist/models/commerce/Auction.ts +6 -0
  25. package/dist/models/commerce/AuctionItem.ts +6 -0
  26. package/dist/models/commerce/Cart.ts +6 -1
  27. package/dist/models/commerce/CartItem.ts +6 -1
  28. package/dist/models/commerce/Category.ts +6 -0
  29. package/dist/models/commerce/Coupon.ts +6 -0
  30. package/dist/models/commerce/DeliveryRoute.ts +6 -1
  31. package/dist/models/commerce/DeliveryStop.ts +6 -1
  32. package/dist/models/commerce/GiftCard.ts +6 -1
  33. package/dist/models/commerce/LicenseKey.ts +6 -1
  34. package/dist/models/commerce/LoyaltyReward.ts +6 -0
  35. package/dist/models/commerce/Manufacturer.ts +6 -0
  36. package/dist/models/commerce/Order.ts +6 -1
  37. package/dist/models/commerce/Payment.ts +6 -1
  38. package/dist/models/commerce/PrintDevice.ts +6 -0
  39. package/dist/models/commerce/Product.ts +6 -0
  40. package/dist/models/commerce/ProductUnit.ts +6 -0
  41. package/dist/models/commerce/ProductVariant.ts +6 -0
  42. package/dist/models/commerce/Review.ts +6 -1
  43. package/dist/models/commerce/ShippingMethod.ts +6 -0
  44. package/dist/models/commerce/ShippingRate.ts +6 -0
  45. package/dist/models/commerce/ShippingZone.ts +6 -0
  46. package/dist/models/commerce/TaxRate.ts +6 -0
  47. package/dist/models/commerce/Transaction.ts +6 -1
  48. package/dist/models/commerce/WaitlistProduct.ts +6 -1
  49. package/dist/models/commerce/WaitlistRestaurant.ts +6 -1
  50. package/dist/models/realtime/Websocket.ts +5 -0
  51. package/dist/ownership.d.ts +96 -0
  52. package/dist/routes.js +1 -1
  53. package/package.json +21 -21
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { customerOwnership, defineModel } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  export default defineModel({
@@ -7,6 +7,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
  belongsTo: ['Product', 'Customer'],
10
+ // Rows belong to the caller's customer record, one hop from the user
11
+ // (stacksjs/stacks#2375). Without this the generated writes are reachable by
12
+ // any authenticated caller for any row.
13
+ ownership: customerOwnership(),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  useTimestamps: true,
@@ -1,4 +1,4 @@
1
- import { defineModel } from '@stacksjs/orm'
1
+ import { customerOwnership, defineModel } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  export default defineModel({
@@ -7,6 +7,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
  belongsTo: ['Customer'],
10
+ // Rows belong to the caller's customer record, one hop from the user
11
+ // (stacksjs/stacks#2375). Without this the generated writes are reachable by
12
+ // any authenticated caller for any row.
13
+ ownership: customerOwnership(),
14
+
10
15
  traits: {
11
16
  useUuid: true,
12
17
  useTimestamps: true,
@@ -7,6 +7,11 @@ export default defineModel({
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
+ // An infrastructure table: rows are written by the system, not on behalf of a
11
+ // caller, so no row has a per-caller owner to scope by. Writes are gated by
12
+ // `middleware` instead. Declared rather than left silent (stacksjs/stacks#2375).
13
+ ownership: false,
14
+
10
15
  traits: {
11
16
  useTimestamps: true,
12
17
  useSeeder: {
@@ -0,0 +1,96 @@
1
+ // The ownership config actually enforced for a model. An explicit
2
+ // `model.ownership` always wins. Otherwise any model with a `team_id`
3
+ // column is auto-scoped to the caller's active team — tenant tables are
4
+ // row-isolated with zero per-model config, while a public catalog table
5
+ // (no team_id, no ownership) resolves to `null` and stays un-scoped.
6
+ //
7
+ // The team is resolved from the request's REAL credential (bearer token or
8
+ // session cookie) via @stacksjs/auth — never from a client-supplied field —
9
+ // so a caller can't widen their own scope by POSTing or ?team_id=-ing another
10
+ // team's id. Lazy import mirrors authedUserFromRequest: avoids a boot-time
11
+ // cycle through @stacksjs/auth.
12
+ export declare function effectiveOwnershipConfig(model: any): any | null;
13
+ /**
14
+ * Adapt a raw request to the `{ bearerToken, cookies }` shape @stacksjs/auth's
15
+ * team resolver expects.
16
+ *
17
+ * The auto-CRUD paths read the Authorization header directly rather than
18
+ * relying on `req.bearerToken()` being wired, so the credential is surfaced
19
+ * from the header and the cookie header is parsed for the session cookie.
20
+ */
21
+ export declare function teamAuthRequest(req: any): { bearerToken: () => string | null, cookies: { get: (name: string) => string | null } };
22
+ /**
23
+ * Rows owned by the caller's customer record.
24
+ *
25
+ * The commerce models hang off `Customer`, not `User` - an order belongs to a
26
+ * customer, and a customer belongs to a user - so the owner value is one hop
27
+ * away and has to be looked up. Returns `null` for a user with no customer
28
+ * record, which `ownsRow` treats as owning nothing rather than as owning
29
+ * everything.
30
+ */
31
+ export declare function customerOwnership(field?: string): {
32
+ field: string
33
+ resolve: (user: any) => Promise<number | null>
34
+ };
35
+ /**
36
+ * Rows the caller owns directly, keyed by the user's own id.
37
+ *
38
+ * For `User` itself, where the owner column IS the primary key: a caller may
39
+ * write their own row and no other. Without this, `User` is the worst case the
40
+ * issue describes - an authenticated caller able to `PATCH /api/users/{any}`.
41
+ */
42
+ export declare function selfOwnership(field?: string): {
43
+ field: string
44
+ resolve: (user: any) => Promise<number | null>
45
+ };
46
+ /**
47
+ * Rows owned through the caller's team, two hops away, via `Site`.
48
+ *
49
+ * A page belongs to a site and a site belongs to a team, so there is no single
50
+ * owner id to compare against - the answer is the SET of site ids the caller's
51
+ * team owns. `ownsRow` accepts an array for exactly this case, and a caller
52
+ * whose team owns no sites gets an empty set, which owns nothing.
53
+ */
54
+ export declare function siteOwnership(field?: string): {
55
+ field: string
56
+ resolve: (user: any, req: any) => Promise<number[]>
57
+ };
58
+ /**
59
+ * The team row the caller is actually a member of.
60
+ *
61
+ * `Team`'s owner column is its own primary key, and membership is what the
62
+ * auth layer already resolves for every other team-scoped model - so this
63
+ * reuses that answer rather than inventing a second notion of which team a
64
+ * caller belongs to.
65
+ */
66
+ export declare function teamMembershipOwnership(field?: string): {
67
+ field: string
68
+ resolve: (user: any, req: any) => Promise<number | null>
69
+ };
70
+ /**
71
+ * Rows owned through a parent row's owner.
72
+ *
73
+ * Some models have no owner of their own and never will: a cart item belongs to
74
+ * a cart, a form field to a form, a transaction to an order. The owner is
75
+ * whoever owns the PARENT, so the answer for the child is the set of parent ids
76
+ * the caller owns - and `ownsRow` already accepts a set.
77
+ *
78
+ * The parent's ownership is resolved with the same rules the parent itself is
79
+ * subject to (`effectiveOwnershipConfig`), rather than restated here. That
80
+ * matters: when `Cart` changes how it decides ownership, `CartItem` follows
81
+ * automatically instead of drifting into a second, staler answer.
82
+ *
83
+ * One hop only, deliberately. A chain of chains is a query per level on every
84
+ * write, and nothing in the framework needs two - the models that look like
85
+ * they might (BoardColumn through Board, Receipt through PrintDevice) have a
86
+ * parent that is itself unscoped, so a second hop would resolve to "no owner"
87
+ * anyway. Those stay denied, which is the honest answer: nothing in the schema
88
+ * says who owns them.
89
+ *
90
+ * @param parentModel Model name as registered, e.g. `'Cart'`.
91
+ * @param field The child's foreign key column, e.g. `'cart_id'`.
92
+ */
93
+ export declare function parentOwnership(parentModel: string, field: string): {
94
+ field: string
95
+ resolve: (user: any, req: any) => Promise<number[]>
96
+ };
package/dist/routes.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- var ke=import.meta.require;import ut from"process";import{route as F}from"@stacksjs/router";import{env as z}from"@stacksjs/env";import{projectPath as be,storagePath as ct}from"@stacksjs/path";import{createQueryBuilder as lt,defaultConfig as Me,setConfig as Fe}from"@stacksjs/query-builder";import{HttpError as U}from"@stacksjs/error-handling";import{log as X}from"@stacksjs/logging";function Xe(e){let t=e;return t?.code==="SQLITE_CONSTRAINT_UNIQUE"||t?.code==="SQLITE_CONSTRAINT"||t?.code==="23505"||t?.errno===1062||/unique|duplicate/i.test(t?.message??"")}function ce(e,t,n){let r=e;if(e instanceof Error&&typeof r.status==="number"&&Number.isInteger(r.status)&&r.status>=400&&r.status<600){let o={error:e.message};if(r.details!==void 0)o.details=r.details;return{status:r.status,body:o}}if(Xe(e))return{status:409,body:{error:`${t} already exists`}};return{status:500,body:{error:`Failed to ${n} ${t}`,detail:String(e)}}}function k(e){return e.replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/([A-Z])([A-Z][a-z])/g,"$1_$2").toLowerCase()}function le(e){let t={};for(let[n,r]of Object.entries(e))t[k(n)]=r;return t}function xe(e,t){let n=String(t||"").replace(/^\/+|\/+$/g,""),r=String(e).replace(/^\/+/,"");return`/api/${n?`${n}/`:""}${r}`}function Oe(e){if(!e)return null;let t=e.attributes??{};if(Object.prototype.hasOwnProperty.call(t,"teamId")||Object.prototype.hasOwnProperty.call(t,"team_id"))return"team_id";return e.belongsTo?.some((n)=>n==="Team")?"team_id":null}function Te(e,t,n){let r=Object.keys(e).find((s)=>k(s)===k(t));if(Array.isArray(n)){if(r===void 0)return{data:e,error:"Ownership value is required"};if(!n.some((s)=>String(s)===String(e[r])))return{data:e,error:"Ownership value is not available to this caller"};return{data:{...e,[t]:e[r]}}}if(n===null||n===void 0)return{data:e,error:"Caller has no ownership identity"};let o={...e};for(let s of Object.keys(o))if(k(s)===k(t))delete o[s];return o[t]=n,{data:o}}function Ae(e){if(!e)return[];let t=Object.entries(e.attributes??{}).filter(([,n])=>n?.fillable===!0).map(([n])=>n);for(let n of e.belongsTo??[]){if(typeof n!=="string"||!n.trim())continue;let r=k(n.trim()).split("_").filter(Boolean);if(r.length===0)continue;let o=`${r[0]}${r.slice(1).map((s)=>`${s[0]?.toUpperCase()}${s.slice(1)}`).join("")}Id`;t.push(o)}return[...new Set(t)]}function de(e,t){if(!e||t.length===0)return{};let n={};for(let r of t){if(r in e){n[r]=e[r];continue}let o=k(r);if(o!==r&&o in e)n[r]=e[o]}return n}function Ye(e,t){if(e?.name!=="date"||t instanceof Date||typeof t!=="string")return t;let n=/^(\d{4})-(\d{2})-(\d{2})$/.exec(t);if(!n)return t;let r=Number(n[1]),o=Number(n[2]),s=Number(n[3]),a=new Date(Date.UTC(r,o-1,s));if(a.getUTCFullYear()!==r||a.getUTCMonth()!==o-1||a.getUTCDate()!==s)return t;return a}function fe(e,t){if(!t.length)return e;let n={...e};for(let r of t)delete n[r],delete n[k(r)];return n}function V(e,t){if(!e||t.length===0)return e;let n={...e};for(let r of t)delete n[r],delete n[k(r)];return n}var Ze=["id","uuid","created_at","updated_at","deleted_at"];function De(e,t){let n=new Map;for(let r of[...Object.keys(e??{}),...Ze]){let o=k(r);if(!/^\w+$/.test(o))continue;n.set(r,o),n.set(o,o)}for(let r of t)n.delete(r),n.delete(k(r));return n}function Pe(e,t,n){if(!t)return e;let r=String(t).split(",").map((s)=>s.trim()).filter(Boolean),o=e;for(let s of r){let a=s.startsWith("-"),p=a?s.slice(1):s;if(!/^\w+$/.test(p))continue;let w=n.get(p);if(!w)continue;o=o.orderBy(w,a?"desc":"asc")}return o}var et={string:{get:(e)=>e!=null?String(e):null,set:(e)=>e!=null?String(e):null},number:{get:(e)=>e!=null?Number(e):null,set:(e)=>e!=null?Number(e):null},integer:{get:(e)=>e!=null?Math.trunc(Number(e)):null,set:(e)=>e!=null?Math.trunc(Number(e)):null},float:{get:(e)=>e!=null?Number.parseFloat(String(e)):null,set:(e)=>e!=null?Number.parseFloat(String(e)):null},boolean:{get:(e)=>e===1||e==="1"||e===!0||e==="true",set:(e)=>e===!0||e===1||e==="1"||e==="true"?1:0},json:{get:(e)=>e==null?null:typeof e==="string"?tt(e):e,set:(e)=>e==null?null:typeof e==="string"?e:JSON.stringify(e)},datetime:{get:(e)=>e?new Date(e):null,set:(e)=>e instanceof Date?e.toISOString():e},date:{get:(e)=>e?new Date(e):null,set:(e)=>e instanceof Date?e.toISOString().split("T")[0]:e},array:{get:(e)=>e==null?[]:Array.isArray(e)?e:typeof e==="string"?nt(e):[],set:(e)=>e==null?null:Array.isArray(e)?JSON.stringify(e):e}};function tt(e){try{return JSON.parse(e)}catch{return e}}function nt(e){try{return JSON.parse(e)}catch{return[]}}function ge(e,t,n){if(!e||typeof e!=="object"||!t||Object.keys(t).length===0)return e;let r={...e};for(let[o,s]of Object.entries(t)){let a=typeof s==="string"?et[s]:s;if(!a||typeof a[n]!=="function")continue;if(Object.prototype.hasOwnProperty.call(r,o))r[o]=a[n](r[o]);let p=k(o);if(p!==o&&Object.prototype.hasOwnProperty.call(r,p))r[p]=a[n](r[p])}return r}function pe(e,t,n){let r=t?.attributes??{},o={};for(let[s,a]of Object.entries(r)){let p=a?.validation?.rule;if(!p||typeof p.validate!=="function")continue;let w=Object.prototype.hasOwnProperty.call(e,s);if(!w&&n==="updating")continue;let c=a!==null&&typeof a==="object"&&Object.prototype.hasOwnProperty.call(a,"default"),I=w?e[s]:c?a.default:void 0,C=Ye(p,I),D=p.validate(C);if(!D?.valid&&Array.isArray(D?.errors)&&D.errors.length>0)o[s]=D.errors.map((B)=>a?.validation?.message?.[B?.code]??B?.message??"invalid")}return Object.keys(o).length===0?{valid:!0}:{valid:!1,errors:o}}function $e(e,t,n){let r=_e(n);return e.find((o)=>o.method===t&&_e(String(o.path??""))===r)}function _e(e){return e.replace(/\{[^}]*\}/g,"{}").replace(/:[^/]+/g,"{}")}function ue(e){if(Array.isArray(e))return e.filter((t)=>typeof t==="string"&&t.length>0);return typeof e==="string"&&e?[e]:[]}function Ne(e){let t=typeof e==="object"&&e!==null&&"middleware"in e,n=e?.middleware;if(!t)return{read:["auth"],write:["auth"],declared:!1};if(n&&typeof n==="object"&&!Array.isArray(n)){let o=n;return{read:"read"in o?ue(o.read):["auth"],write:"write"in o?ue(o.write):["auth"],declared:!0}}let r=ue(n);return{read:r,write:r,declared:!0}}var Se=15,rt=100;function Ie(e){let t=Number.parseInt(e.get("page")||String(1),10),n=Number.isFinite(t)?Math.max(1,t):1,r=Number.parseInt(e.get("per_page")||String(Se),10),o=Math.min(Number.isFinite(r)?Math.max(1,r):Se,rt);return{page:n,perPage:o,offset:(n-1)*o}}function oe(e,t){let n=new URL(e.toString());return n.searchParams.set("page",String(t)),`${n.pathname}${n.search}`}function se(e,t,n,r,o,s){let a=(t-1)*n,p=r===0,w={page:t,per_page:n,from:p?null:a+1,to:p?null:a+r,has_more_pages:o,prev_page_url:t>1?oe(e,t-1):null,next_page_url:o?oe(e,t+1):null};if(s!==void 0&&!Number.isNaN(s)){let c=Math.max(1,Math.ceil(s/n));w.total=s,w.last_page=c,w.first_page_url=oe(e,1),w.last_page_url=oe(e,c)}return w}function ye(e,t,n,r,o,s){let{page:a,...p}=se(e,t,n,r,o,s);return{current_page:a,...p}}function me(e,t){return(t??e??"").toString().trim().toLowerCase()==="deny"?"deny":"warn"}function Ce(e,t){if(e.length===0)return null;let n=[...e].sort().join(", ");if(t==="deny")return`[orm] security.api.rowScoping is 'deny': NOT registering store/update/destroy for ${e.length} model(s) `+`with no row-level scoping \u2014 ${n}. Give a model an \`ownership\` config or a \`team_id\` column to restore its writes.`;return`[orm] ${e.length} model(s) publish mutating routes with no row-level scoping, so an authenticated caller can write any row \u2014 `+`${n}. 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.`}import{readdirSync as ot}from"fs";import{basename as st,extname as ve,join as it}from"path";function at(e){let t=[],n=(r)=>{let o;try{o=ot(r,{withFileTypes:!0})}catch{return}for(let s of o){let a=it(r,s.name);if(s.isDirectory()){n(a);continue}if(![".ts",".js"].includes(ve(s.name)))continue;if(s.name.startsWith(".")||s.name.startsWith("index"))continue;t.push(a)}};return n(e),t.sort()}async function Ee(e,t,n){for(let r of at(e))try{let o=ve(r),s=await import(`${r}?t=${Date.now()}`),a=s.default??s,p=a.name??st(r,o);t[p]={...a,name:p}}catch(o){n?.(r,o)}}async function je(e){let t={};return await Ee(e.defaultsRoot,t,e.onImportError),await Ee(e.userRoot,t,e.onImportError),t}var dt=be("config/qb.ts");try{let e=(await import(dt)).default;Fe(e??Me)}catch{X.debug("[orm] No config/qb.ts override found - deriving config from DB_CONNECTION");let e=z.DB_CONNECTION||"sqlite";Fe({...Me,dialect:e,database:e==="sqlite"?{database:z.DB_DATABASE_PATH||"database/stacks.sqlite"}:{database:z.DB_DATABASE||"stacks",host:z.DB_HOST||"127.0.0.1",port:z.DB_PORT||(e==="postgres"?5432:3306),username:z.DB_USERNAME||(e==="postgres"?"postgres":"root"),password:z.DB_PASSWORD||""}})}var he=await je({defaultsRoot:ct("framework/defaults/app/Models"),userRoot:be("app/Models"),onImportError:(e,t)=>X.warn(`[orm] Could not load model ${e}: ${t.message}`)}),ft=lt(),A=ft;function H(e,t){let n=$e(F.routes,e,t);if(!n)return!1;let r=String(n.path??"");return X.info(`[orm] Skipping generated ${e} ${t} - already defined${r&&r!==t?` as ${r}`:""}.`),!0}function ze(e){if(!e.attributes)return[];return Object.entries(e.attributes).filter(([t,n])=>n.hidden===!0).map(([t])=>t)}function Ue(e){if(e.endsWith("y")&&!/[aeiou]y$/i.test(e))return`${e.slice(0,-1)}ies`;if(/(?:s|x|z|ch|sh)$/i.test(e))return`${e}es`;return`${e}s`}async function Be(e,t,n,r){if(!e.length)return e;let o=n.split(",").map((c)=>c.trim()).filter(Boolean);if(!o.length)return e;let s=Array.isArray(t.belongsTo)?t.belongsTo:[],a=Array.isArray(t.hasMany)?t.hasMany:[],p=Array.isArray(t.hasOne)?t.hasOne:[],w=new Map;for(let c of s)w.set(k(c),{kind:"belongsTo",modelName:c});for(let c of p)w.set(k(c),{kind:"hasOne",modelName:c});for(let c of a)w.set(Ue(k(c)),{kind:"hasMany",modelName:c});for(let c of o){let I=w.get(c);if(!I)continue;let C=r[I.modelName];if(!C)continue;let D=C.table||Ue(k(I.modelName)),B=ze(C),Y=(Q)=>V(ne(Q,C),B);if(I.kind==="belongsTo"||I.kind==="hasOne"){let Q=`${k(I.modelName)}_id`,E=[...new Set(e.map((S)=>S[Q]).filter((S)=>S!=null))];if(!E.length){for(let S of e)S[c]=null;continue}let J=await A.selectFrom(D).whereIn("id",E).get(),W=new Map;for(let S of J??[])W.set(String(S.id),Y(S));for(let S of e)S[c]=W.get(String(S[Q]))??null}else{let Q=String(t.name??""),E=`${k(Q)}_id`,J=[...new Set(e.map((x)=>x.id).filter((x)=>x!=null))];if(!J.length){for(let x of e)x[c]=[];continue}let W=await A.selectFrom(D).whereIn(E,J).get(),S=new Map;for(let x of W??[]){let Z=String(x[E]),f=S.get(Z)??[];f.push(Y(x)),S.set(Z,f)}for(let x of e)x[c]=S.get(String(x.id))??[]}}return e}async function Qe(e,t){let n=t?.set;if(!n||typeof n!=="object")return e;let r={...e};for(let[o,s]of Object.entries(n)){if(typeof s!=="function"||!(o in r))continue;try{r[o]=await s(r)}catch{}}return r}function We(e,t){return ge(e,t?.casts,"set")}function ne(e,t){return ge(e,t?.casts,"get")}function l(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":"application/json"}})}var ie=1048576;async function we(e){if(Number(e.headers?.get?.("content-length")??0)>ie)throw new U(413,`Request body exceeds ${ie}-byte limit`);if(e.jsonBody&&typeof e.jsonBody==="object")return e.jsonBody;if(e.formBody&&typeof e.formBody==="object")return e.formBody;try{let n=await e.clone().text();if(n.length>ie)throw new U(413,`Request body exceeds ${ie}-byte limit`);if(!n)return{};let r=JSON.parse(n);return r&&typeof r==="object"&&!Array.isArray(r)?r:{}}catch(n){if(n instanceof U)throw n;throw new U(400,`Invalid JSON body: ${n.message}`)}}function ae(e){if(e==null)return null;if(typeof e==="number")return Number.isFinite(e)&&e>0?e:null;if(typeof e==="string"){let t=e.trim();if(!t)return null;if(/^\d+$/.test(t)){let n=Number(t);return Number.isFinite(n)&&n>0?n:null}return t}return null}function He(e){let t=e.bearerToken;if(typeof t==="function"){let r=t.call(e);if(r)return r}let n=e.headers?.get?.("authorization")||e.headers?.get?.("Authorization")||"";if(typeof n==="string"&&n.startsWith("Bearer "))return n.substring(7);return null}async function G(e){let t=e._authenticatedUser;if(t)return t;let n=He(e);if(!n)return null;try{let{Auth:r}=await import("@stacksjs/auth"),o=await r.getUserFromToken(n);if(o)e._authenticatedUser=o;return o||null}catch{return null}}function te(e,t){if(e==null||t==null)return!1;if(Array.isArray(t))return t.some((n)=>String(n)===String(e));return String(e)===String(t)}function gt(e){let t=He(e),n=e.headers?.get?.("cookie")||"";return{bearerToken:()=>t,cookies:{get:(r)=>{for(let o of n.split(";")){let s=o.indexOf("=");if(s===-1)continue;if(o.slice(0,s).trim()===r)return decodeURIComponent(o.slice(s+1).trim())}return null}}}}function Je(e){if(e?.ownership)return e.ownership;let t=Oe(e);if(!t)return null;return{field:t,resolve:async(n,r)=>{let{resolveAuthenticatedTeamId:o}=await import("@stacksjs/auth");return o(gt(r))}}}async function K(e,t,n){let r=Je(e);if(!r||!r.field||typeof r.resolve!=="function")return{enforced:!1,value:null,field:"",bypass:!1};let o=typeof r.bypass==="function"?!!r.bypass(t,n):!1,s=null;try{s=await r.resolve(t,n)}catch{s=null}return{enforced:!0,value:s,field:String(r.field),bypass:o}}async function Le(e,t,n,r){let o=e?.authedFill?.[t];if(!o)return{};if(typeof o==="function")try{let s=await o(n,r);return s&&typeof s==="object"?s:{}}catch{return{}}if(typeof o==="object"){let s={};for(let[a,p]of Object.entries(o))try{s[a]=typeof p==="function"?await p(n,r):p}catch{}return s}return{}}var qe=await(async()=>{let e=ut.env.STACKS_API_ROW_SCOPING;try{let t=(await import(be("config/security.ts"))).default;return me(t?.api?.rowScoping,e)}catch{return me(void 0,e)}})(),Ve=[];for(let[e,t]of Object.entries(he)){let n=t.traits?.useApi;if(!n)continue;let r=typeof n==="object"?n:{},o=r.uri||t.table||e.toLowerCase()+"s",s=r.routes||["index","show","store","update","destroy"],a=t.table||o,p=Ae(t),w=ze(t),c=xe(o,r.prefix),I=De(t.attributes,w),{read:C,write:D,declared:B}=Ne(n),Y=["store","update","destroy"].some((f)=>s.includes(f));if(Y&&B&&D.length===0)X.warn(`[orm] ${e}: registering UNAUTHENTICATED mutating routes at ${c} (explicit \`middleware: []\` opt-out)`);if(["index","show"].some((f)=>s.includes(f))&&B&&C.length===0)X.debug(`[orm] ${e}: registering public read routes at ${c} (declared \`middleware.read: []\`)`);let E=!!Je(t),J=E&&!C.includes("auth")?["auth",...C]:C,W=Y&&!E;if(W)Ve.push(e);let S=W&&qe==="deny",x=(f,i=J)=>{let u=f;for(let d of i)if(u&&typeof u.middleware==="function")u=u.middleware(d);return u};if(s.includes("index")&&!H("GET",c))x(F.get(c,async(f)=>{try{let i=new URL(f.url),{page:u,perPage:d,offset:h}=Ie(i.searchParams),m=i.searchParams.get("sort"),g=A.selectFrom(a);g=Pe(g,m,I);let N=new Set(["page","per_page","sort","fields","search","include","limit","offset","with_count","withTrashed","onlyTrashed"]);for(let[O,T]of i.searchParams.entries()){if(N.has(O)||!T)continue;if(!/^[a-z_][a-z0-9_]*$/i.test(O))continue;let L=I.get(O);if(L)g=g.where(L,"=",T)}let R=i.searchParams.get("search");if(R&&p.length>0){let O=p.slice(0,5);g=g.where((T)=>{for(let L of O)T=T.orWhere(k(L),"like",`%${R}%`);return T})}let _=i.searchParams.get("fields");if(_){let O=_.split(",").map((T)=>T.trim()).filter((T)=>/^[a-z_][a-z0-9_]*$/i.test(T)).map((T)=>k(T));if(O.length>0)g=g.select(O)}let P=!!t.traits?.useSoftDeletes,b=i.searchParams.get("withTrashed")==="true",v=i.searchParams.get("onlyTrashed")==="true";if(P&&!b)g=v?g.whereNotNull("deleted_at"):g.whereNull("deleted_at");let y=E?await K(t,await G(f),f):{enforced:!1,value:null,field:"",bypass:!1};if(y.enforced&&!y.bypass){if(y.value==null||Array.isArray(y.value)&&y.value.length===0){let O=ye(i,u,d,0,!1,void 0);return new Response(JSON.stringify({data:[],...O,meta:se(i,u,d,0,!1,void 0)}),{status:200,headers:{"Content-Type":"application/json","Cache-Control":"private, no-store",Vary:"Authorization"}})}g=Array.isArray(y.value)?g.where(y.field,"in",y.value):g.where(y.field,"=",y.value)}let j=await g.limit(d+1).offset(h).get()||[],M=j.length>d,ee=(M?j.slice(0,d):j).map((O)=>V(ne(O,t),w)),Re=i.searchParams.get("include");if(Re)ee=await Be(ee,t,Re,he);let Ge=i.searchParams.get("with_count")==="true",q;if(Ge)try{let O=A.selectFrom(a);if(y.enforced&&!y.bypass&&y.value!=null)O=Array.isArray(y.value)?O.where(y.field,"in",y.value):O.where(y.field,"=",y.value);let T=await O.count(),L=typeof T==="number"?T:Number(T?.count??T);if(Number.isFinite(L))q=L}catch{}let re={"Content-Type":"application/json"};if(q!==void 0&&!Number.isNaN(q))re["X-Total-Count"]=String(q);re["Cache-Control"]=y.enforced&&!y.bypass?"private, no-store":"public, max-age=15, must-revalidate",re.Vary="Authorization";let Ke=ye(i,u,d,ee.length,M,q);return new Response(JSON.stringify({data:ee,...Ke,meta:se(i,u,d,ee.length,M,q)}),{status:200,headers:re})}catch(i){if(i instanceof U){let u={error:i.message};if(i.details!==void 0)u.details=i.details;return l(u,i.status||400)}return l({error:`Failed to fetch ${o}`,detail:String(i)},500)}}));if(s.includes("show")&&!H("GET",`${c}/{id}`))x(F.get(`${c}/{id}`,async(f)=>{try{let i=ae(f.params?.id);if(i==null)return l({error:"Invalid ID parameter"},400);let u=await A.selectFrom(a).where({id:i}).executeTakeFirst();if(!u)return l({error:`${e} not found`},404);let d=new URL(f.url);if(t.traits?.useSoftDeletes&&u.deleted_at&&d.searchParams.get("withTrashed")!=="true")return l({error:`${e} not found`},404);let h=E?await K(t,await G(f),f):{enforced:!1,value:null,field:"",bypass:!1};if(h.enforced&&!h.bypass){if(h.value==null||!te(u[h.field],h.value))return l({error:`${e} not found`},404)}let m=V(ne(u,t),w),g=d.searchParams.get("include");if(g){let[b]=await Be([m],t,g,he);m=b}let N=m?.updated_at||m?.created_at,R=N?`W/"${m.id}-${String(N)}"`:void 0,_=f.headers?.get?.("if-none-match");if(R&&_&&_===R)return new Response(null,{status:304,headers:{ETag:R}});let P={"Content-Type":"application/json"};if(R)P.ETag=R;return P["Cache-Control"]=h.enforced&&!h.bypass?"private, no-store":"public, max-age=30, must-revalidate",P.Vary="Authorization",new Response(JSON.stringify({data:m}),{status:200,headers:P})}catch(i){if(i instanceof U){let u={error:i.message};if(i.details!==void 0)u.details=i.details;return l(u,i.status||400)}return l({error:`Failed to fetch ${e}`,detail:String(i)},500)}}));if(!S&&s.includes("store")&&!H("POST",c))x(F.post(c,async(f)=>{try{let i=await we(f),u=fe(i,w),d=de(u,p),h=await G(f);if(h){let b=await Le(t,"creating",h,f);for(let[v,y]of Object.entries(b))if(y!==void 0&&y!==null&&d[v]===void 0)d[v]=y}let m=await K(t,h,f);if(m.enforced&&!m.bypass){if(!h)return l({error:"Auth required"},401);let b=Te(d,m.field,m.value);if(b.error)return l({error:b.error},403);d=b.data}if(Object.keys(d).length===0)return l({error:"No fillable fields provided"},422);let g=pe(d,t,"creating");if(!g.valid)return l({success:!1,message:"Validation failed",errors:g.errors},422);if(t.traits?.useTimestamps!==!1){let b=new Date().toISOString();d.created_at=b,d.updated_at=b}let N=await Qe(d,t),R=le(We(N,t)),_=await A.insertInto(a).values(R).execute(),P=R;try{let b=_?.lastInsertRowid??_?.insertId??_;if(b){let v=await A.selectFrom(a).where({id:b}).executeTakeFirst();if(v)P=v}}catch{}return l({data:V(ne(P,t),w)},201)}catch(i){let{status:u,body:d}=ce(i,e,"create");return l(d,u)}}),D);if(!S&&s.includes("update")){let f=async(i)=>{try{let u=ae(i.params?.id);if(u==null)return l({error:"Invalid ID parameter"},400);let d=await we(i),h=fe(d,w),m=de(h,p),g=await G(i);if(g){let y=await Le(t,"updating",g,i);for(let[j,M]of Object.entries(y))if(M!==void 0&&M!==null&&m[j]===void 0)m[j]=M}if(Object.keys(m).length===0)return l({error:"No fillable fields provided"},422);let N=pe(m,t,"updating");if(!N.valid)return l({success:!1,message:"Validation failed",errors:N.errors},422);let R=await A.selectFrom(a).where({id:u}).executeTakeFirst();if(!R)return l({error:`${e} not found`},404);let _=await K(t,g,i);if(_.enforced&&!_.bypass){if(!g)return l({error:"Auth required"},401);let y=R[_.field];if(!te(y,_.value))return l({error:`Not your ${e}`},403);let j=Object.keys(m).find((M)=>k(M)===k(_.field));if(j!==void 0&&!te(m[j],_.value))return l({error:`Cannot reassign ${e} ownership`},403)}if(t.traits?.useTimestamps!==!1)m.updated_at=new Date().toISOString();let P=await Qe(m,t),b=le(We(P,t));await A.updateTable(a).set(b).where({id:u}).execute();let v={...R,...b};try{let y=await A.selectFrom(a).where({id:u}).executeTakeFirst();if(y)v=y}catch{}return l({data:V(ne(v,t),w)})}catch(u){let{status:d,body:h}=ce(u,e,"update");return l(h,d)}};if(!H("PUT",`${c}/{id}`))x(F.put(`${c}/{id}`,f),D);if(!H("PATCH",`${c}/{id}`))x(F.patch(`${c}/{id}`,f),D)}let Z=!!t.traits?.useSoftDeletes;if(!S&&s.includes("destroy")&&!H("DELETE",`${c}/{id}`))x(F.delete(`${c}/{id}`,async(f)=>{try{let i=ae(f.params?.id);if(i==null)return l({error:"Invalid ID parameter"},400);let u=await A.selectFrom(a).where({id:i}).executeTakeFirst();if(!u)return l({error:`${e} not found`},404);let d=await G(f),h=await K(t,d,f);if(h.enforced&&!h.bypass){if(!d)return l({error:"Auth required"},401);let m=u[h.field];if(!te(m,h.value))return l({error:`Not your ${e}`},403)}if(Z){let m=new Date().toISOString();await A.updateTable(a).set({deleted_at:m,updated_at:m}).where({id:i}).execute()}else await A.deleteFrom(a).where({id:i}).execute();return new Response(null,{status:204})}catch(i){if(i instanceof U){let u={error:i.message};if(i.details!==void 0)u.details=i.details;return l(u,i.status||400)}return l({error:`Failed to delete ${e}`,detail:String(i)},500)}}),D);if(!S&&s.includes("destroy")&&!H("POST",`${c}/bulk-delete`))x(F.post(`${c}/bulk-delete`,async(f)=>{try{let u=(await we(f))?.ids;if(!Array.isArray(u)||u.length===0)return l({error:"An array of IDs is required"},422);if(u.length>100)return l({error:"Cannot delete more than 100 records at once"},422);let d=[],h=[];for(let R of u){let _=ae(R);if(_!=null)d.push(_);else h.push(R)}if(h.length>0)return l({error:"Invalid IDs in batch",invalid:h},422);let m=await G(f),g=await K(t,m,f);if(g.enforced&&!g.bypass){if(!m)return l({error:"Auth required"},401);if(g.value==null)return l({error:"Caller has no ownership identity"},403);let R=await A.selectFrom(a).select(["id",g.field]).execute(),_=new Set(R.filter((b)=>te(b[g.field],g.value)).map((b)=>String(b.id))),P=d.filter((b)=>!_.has(String(b)));if(P.length>0)return l({error:`Cannot delete ${e} you don't own`,ids:P},403)}let N=new Date().toISOString();for(let R of d)if(Z)await A.updateTable(a).set({deleted_at:N,updated_at:N}).where({id:R}).execute();else await A.deleteFrom(a).where({id:R}).execute();return l({message:`Successfully deleted ${d.length} ${o}`})}catch(i){if(i instanceof U){let u={error:i.message};if(i.details!==void 0)u.details=i.details;return l(u,i.status||400)}return l({error:`Failed to bulk delete ${o}`,detail:String(i)},500)}}),D)}{let e=Ce(Ve,qe);if(e)X.warn(e)}var $t=F;export{$t as default};
2
+ var H=import.meta.require;import dt from"process";import{route as U}from"@stacksjs/router";import{env as J}from"@stacksjs/env";import{projectPath as Se,storagePath as ft}from"@stacksjs/path";import{createQueryBuilder as gt,defaultConfig as We,setConfig as Le}from"@stacksjs/query-builder";import{HttpError as B}from"@stacksjs/error-handling";import{log as V}from"@stacksjs/logging";function et(e){let t=e;return t?.code==="SQLITE_CONSTRAINT_UNIQUE"||t?.code==="SQLITE_CONSTRAINT"||t?.code==="23505"||t?.errno===1062||/unique|duplicate/i.test(t?.message??"")}function fe(e,t,n){let r=e;if(e instanceof Error&&typeof r.status==="number"&&Number.isInteger(r.status)&&r.status>=400&&r.status<600){let s={error:e.message};if(r.details!==void 0)s.details=r.details;return{status:r.status,body:s}}if(et(e))return{status:409,body:{error:`${t} already exists`}};return{status:500,body:{error:`Failed to ${n} ${t}`,detail:String(e)}}}function _(e){return e.replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/([A-Z])([A-Z][a-z])/g,"$1_$2").toLowerCase()}function ge(e){let t={};for(let[n,r]of Object.entries(e))t[_(n)]=r;return t}function Ae(e,t){let n=String(t||"").replace(/^\/+|\/+$/g,""),r=String(e).replace(/^\/+/,"");return`/api/${n?`${n}/`:""}${r}`}function Pe(e){if(!e)return null;let t=e.attributes??{};if(Object.prototype.hasOwnProperty.call(t,"teamId")||Object.prototype.hasOwnProperty.call(t,"team_id"))return"team_id";return e.belongsTo?.some((n)=>n==="Team")?"team_id":null}function De(e){if(!e)return null;let t=e.attributes??{};if(Object.prototype.hasOwnProperty.call(t,"userId")||Object.prototype.hasOwnProperty.call(t,"user_id"))return"user_id";return e.belongsTo?.some((n)=>n==="User")?"user_id":null}function $e(e){return e?.ownership===!1}function Ie(e,t,n){let r=Object.keys(e).find((o)=>_(o)===_(t));if(Array.isArray(n)){if(r===void 0)return{data:e,error:"Ownership value is required"};if(!n.some((o)=>String(o)===String(e[r])))return{data:e,error:"Ownership value is not available to this caller"};return{data:{...e,[t]:e[r]}}}if(n===null||n===void 0)return{data:e,error:"Caller has no ownership identity"};let s={...e};for(let o of Object.keys(s))if(_(o)===_(t))delete s[o];return s[t]=n,{data:s}}function Ne(e){if(!e)return[];let t=Object.entries(e.attributes??{}).filter(([,n])=>n?.fillable===!0).map(([n])=>n);for(let n of e.belongsTo??[]){if(typeof n!=="string"||!n.trim())continue;let r=_(n.trim()).split("_").filter(Boolean);if(r.length===0)continue;let s=`${r[0]}${r.slice(1).map((o)=>`${o[0]?.toUpperCase()}${o.slice(1)}`).join("")}Id`;t.push(s)}return[...new Set(t)]}function pe(e,t){if(!e||t.length===0)return{};let n={};for(let r of t){if(r in e){n[r]=e[r];continue}let s=_(r);if(s!==r&&s in e)n[r]=e[s]}return n}function tt(e,t){if(e?.name!=="date"||t instanceof Date||typeof t!=="string")return t;let n=/^(\d{4})-(\d{2})-(\d{2})$/.exec(t);if(!n)return t;let r=Number(n[1]),s=Number(n[2]),o=Number(n[3]),a=new Date(Date.UTC(r,s-1,o));if(a.getUTCFullYear()!==r||a.getUTCMonth()!==s-1||a.getUTCDate()!==o)return t;return a}function ye(e,t){if(!t.length)return e;let n={...e};for(let r of t)delete n[r],delete n[_(r)];return n}function Y(e,t){if(!e||t.length===0)return e;let n={...e};for(let r of t)delete n[r],delete n[_(r)];return n}var nt=["id","uuid","created_at","updated_at","deleted_at"];function ve(e,t){let n=new Map;for(let r of[...Object.keys(e??{}),...nt]){let s=_(r);if(!/^\w+$/.test(s))continue;n.set(r,s),n.set(s,s)}for(let r of t)n.delete(r),n.delete(_(r));return n}function Ce(e,t,n){if(!t)return e;let r=String(t).split(",").map((o)=>o.trim()).filter(Boolean),s=e;for(let o of r){let a=o.startsWith("-"),l=a?o.slice(1):o;if(!/^\w+$/.test(l))continue;let w=n.get(l);if(!w)continue;s=s.orderBy(w,a?"desc":"asc")}return s}var rt={string:{get:(e)=>e!=null?String(e):null,set:(e)=>e!=null?String(e):null},number:{get:(e)=>e!=null?Number(e):null,set:(e)=>e!=null?Number(e):null},integer:{get:(e)=>e!=null?Math.trunc(Number(e)):null,set:(e)=>e!=null?Math.trunc(Number(e)):null},float:{get:(e)=>e!=null?Number.parseFloat(String(e)):null,set:(e)=>e!=null?Number.parseFloat(String(e)):null},boolean:{get:(e)=>e===1||e==="1"||e===!0||e==="true",set:(e)=>e===!0||e===1||e==="1"||e==="true"?1:0},json:{get:(e)=>e==null?null:typeof e==="string"?st(e):e,set:(e)=>e==null?null:typeof e==="string"?e:JSON.stringify(e)},datetime:{get:(e)=>e?new Date(e):null,set:(e)=>e instanceof Date?e.toISOString():e},date:{get:(e)=>e?new Date(e):null,set:(e)=>e instanceof Date?e.toISOString().split("T")[0]:e},array:{get:(e)=>e==null?[]:Array.isArray(e)?e:typeof e==="string"?ot(e):[],set:(e)=>e==null?null:Array.isArray(e)?JSON.stringify(e):e}};function st(e){try{return JSON.parse(e)}catch{return e}}function ot(e){try{return JSON.parse(e)}catch{return[]}}function me(e,t,n){if(!e||typeof e!=="object"||!t||Object.keys(t).length===0)return e;let r={...e};for(let[s,o]of Object.entries(t)){let a=typeof o==="string"?rt[o]:o;if(!a||typeof a[n]!=="function")continue;if(Object.prototype.hasOwnProperty.call(r,s))r[s]=a[n](r[s]);let l=_(s);if(l!==s&&Object.prototype.hasOwnProperty.call(r,l))r[l]=a[n](r[l])}return r}function we(e,t,n){let r=t?.attributes??{},s={};for(let[o,a]of Object.entries(r)){let l=a?.validation?.rule;if(!l||typeof l.validate!=="function")continue;let w=Object.prototype.hasOwnProperty.call(e,o);if(!w&&n==="updating")continue;let c=a!==null&&typeof a==="object"&&Object.prototype.hasOwnProperty.call(a,"default"),P=w?e[o]:c?a.default:void 0,v=tt(l,P),D=l.validate(v);if(!D?.valid&&Array.isArray(D?.errors)&&D.errors.length>0)s[o]=D.errors.map((Q)=>a?.validation?.message?.[Q?.code]??Q?.message??"invalid")}return Object.keys(s).length===0?{valid:!0}:{valid:!1,errors:s}}function Ee(e,t,n){let r=Oe(n);return e.find((s)=>s.method===t&&Oe(String(s.path??""))===r)}function Oe(e){return e.replace(/\{[^}]*\}/g,"{}").replace(/:[^/]+/g,"{}")}function de(e){if(Array.isArray(e))return e.filter((t)=>typeof t==="string"&&t.length>0);return typeof e==="string"&&e?[e]:[]}function je(e){let t=typeof e==="object"&&e!==null&&"middleware"in e,n=e?.middleware;if(!t)return{read:["auth"],write:["auth"],declared:!1};if(n&&typeof n==="object"&&!Array.isArray(n)){let s=n;return{read:"read"in s?de(s.read):["auth"],write:"write"in s?de(s.write):["auth"],declared:!0}}let r=de(n);return{read:r,write:r,declared:!0}}var Te=15,it=100;function Me(e){let t=Number.parseInt(e.get("page")||String(1),10),n=Number.isFinite(t)?Math.max(1,t):1,r=Number.parseInt(e.get("per_page")||String(Te),10),s=Math.min(Number.isFinite(r)?Math.max(1,r):Te,it);return{page:n,perPage:s,offset:(n-1)*s}}function ie(e,t){let n=new URL(e.toString());return n.searchParams.set("page",String(t)),`${n.pathname}${n.search}`}function ae(e,t,n,r,s,o){let a=(t-1)*n,l=r===0,w={page:t,per_page:n,from:l?null:a+1,to:l?null:a+r,has_more_pages:s,prev_page_url:t>1?ie(e,t-1):null,next_page_url:s?ie(e,t+1):null};if(o!==void 0&&!Number.isNaN(o)){let c=Math.max(1,Math.ceil(o/n));w.total=o,w.last_page=c,w.first_page_url=ie(e,1),w.last_page_url=ie(e,c)}return w}function he(e,t,n,r,s,o){let{page:a,...l}=ae(e,t,n,r,s,o);return{current_page:a,...l}}function be(e,t){return(t??e??"").toString().trim().toLowerCase()==="warn"?"warn":"deny"}function Fe(e,t){if(e.length===0)return null;let n=[...e].sort().join(", ");if(t==="deny")return`[orm] security.api.rowScoping is 'deny': NOT registering store/update/destroy for ${e.length} model(s) `+`with no row-level scoping \u2014 ${n}. Give a model an \`ownership\` config or a \`team_id\` column to restore its writes.`;return`[orm] ${e.length} model(s) publish mutating routes with no row-level scoping, so an authenticated caller can write any row \u2014 `+`${n}. 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.`}import{readdirSync as at}from"fs";import{basename as ut,extname as Be,join as lt}from"path";function ct(e){let t=[],n=(r)=>{let s;try{s=at(r,{withFileTypes:!0})}catch{return}for(let o of s){let a=lt(r,o.name);if(o.isDirectory()){n(a);continue}if(![".ts",".js"].includes(Be(o.name)))continue;if(o.name.startsWith(".")||o.name.startsWith("index"))continue;t.push(a)}};return n(e),t.sort()}async function Ue(e,t,n){for(let r of ct(e))try{let s=Be(r),o=await import(`${r}?t=${Date.now()}`),a=o.default??o,l=a.name??ut(r,s);t[l]={...a,name:l}}catch(s){n?.(r,s)}}async function Qe(e){let t={};return await Ue(e.defaultsRoot,t,e.onImportError),await Ue(e.userRoot,t,e.onImportError),t}function ue(e){if(e?.ownership===!1)return null;if(e?.ownership)return e.ownership;let t=Pe(e);if(t)return{field:t,resolve:async(r,s)=>{let{resolveAuthenticatedTeamId:o}=await import("@stacksjs/auth");return o(Re(s))}};let n=De(e);if(n)return{field:n,resolve:async(r)=>r?.id??null};return null}function Re(e){let t=e?.headers?.get?.("authorization"),n=t&&/^Bearer\s+/i.test(t)?t.replace(/^Bearer\s+/i,"").trim():null,r=e?.headers?.get?.("cookie")||"";return{bearerToken:()=>n,cookies:{get:(s)=>{for(let o of r.split(";")){let a=o.indexOf("=");if(a===-1)continue;if(o.slice(0,a).trim()===s)return decodeURIComponent(o.slice(a+1).trim())}return null}}}}function xt(e="customer_id"){return{field:e,resolve:async(t)=>{let n=t?.id;if(n==null)return null;try{let{Customer:r}=await import("@stacksjs/orm");return(await r.where("user_id",n).first())?.id??null}catch{return null}}}}function Ot(e="id"){return{field:e,resolve:async(t)=>t?.id??null}}function Tt(e="site_id"){return{field:e,resolve:async(t,n)=>{try{let[{resolveAuthenticatedTeamId:r},{Site:s}]=await Promise.all([import("@stacksjs/auth"),import("@stacksjs/orm")]),o=await r(Re(n));if(o==null)return[];return(await s.where("team_id",o).get()??[]).map((l)=>l.id).filter((l)=>l!=null)}catch{return[]}}}}function At(e="id"){return{field:e,resolve:async(t,n)=>{try{let{resolveAuthenticatedTeamId:r}=await import("@stacksjs/auth");return await r(Re(n))}catch{return null}}}}function Pt(e,t){return{field:t,resolve:async(n,r)=>{try{let s=await import("@stacksjs/orm"),o=s.models?.[e]??s[e]?.definition??null,a=ue(o);if(!a?.field||typeof a.resolve!=="function")return[];let l=await a.resolve(n,r);if(l==null||Array.isArray(l)&&l.length===0)return[];let w=s[e];if(!w)return[];return((Array.isArray(l)?await w.whereIn(a.field,l).get():await w.where(a.field,l).get())??[]).map((P)=>P.id).filter((P)=>P!=null)}catch{return[]}}}}var pt=Se("config/qb.ts");try{let e=(await import(pt)).default;Le(e??We)}catch{V.debug("[orm] No config/qb.ts override found - deriving config from DB_CONNECTION");let e=J.DB_CONNECTION||"sqlite";Le({...We,dialect:e,database:e==="sqlite"?{database:J.DB_DATABASE_PATH||"database/stacks.sqlite"}:{database:J.DB_DATABASE||"stacks",host:J.DB_HOST||"127.0.0.1",port:J.DB_PORT||(e==="postgres"?5432:3306),username:J.DB_USERNAME||(e==="postgres"?"postgres":"root"),password:J.DB_PASSWORD||""}})}var _e=await Qe({defaultsRoot:ft("framework/defaults/app/Models"),userRoot:Se("app/Models"),onImportError:(e,t)=>V.warn(`[orm] Could not load model ${e}: ${t.message}`)}),yt=gt(),A=yt;function q(e,t){let n=Ee(U.routes,e,t);if(!n)return!1;let r=String(n.path??"");return V.info(`[orm] Skipping generated ${e} ${t} - already defined${r&&r!==t?` as ${r}`:""}.`),!0}function Ge(e){if(!e.attributes)return[];return Object.entries(e.attributes).filter(([t,n])=>n.hidden===!0).map(([t])=>t)}function ze(e){if(e.endsWith("y")&&!/[aeiou]y$/i.test(e))return`${e.slice(0,-1)}ies`;if(/(?:s|x|z|ch|sh)$/i.test(e))return`${e}es`;return`${e}s`}async function He(e,t,n,r){if(!e.length)return e;let s=n.split(",").map((c)=>c.trim()).filter(Boolean);if(!s.length)return e;let o=Array.isArray(t.belongsTo)?t.belongsTo:[],a=Array.isArray(t.hasMany)?t.hasMany:[],l=Array.isArray(t.hasOne)?t.hasOne:[],w=new Map;for(let c of o)w.set(_(c),{kind:"belongsTo",modelName:c});for(let c of l)w.set(_(c),{kind:"hasOne",modelName:c});for(let c of a)w.set(ze(_(c)),{kind:"hasMany",modelName:c});for(let c of s){let P=w.get(c);if(!P)continue;let v=r[P.modelName];if(!v)continue;let D=v.table||ze(_(P.modelName)),Q=Ge(v),G=(W)=>Y(se(W,v),Q);if(P.kind==="belongsTo"||P.kind==="hasOne"){let W=`${_(P.modelName)}_id`,C=[...new Set(e.map((S)=>S[W]).filter((S)=>S!=null))];if(!C.length){for(let S of e)S[c]=null;continue}let K=await A.selectFrom(D).whereIn("id",C).get(),L=new Map;for(let S of K??[])L.set(String(S.id),G(S));for(let S of e)S[c]=L.get(String(S[W]))??null}else{let W=String(t.name??""),C=`${_(W)}_id`,K=[...new Set(e.map((T)=>T.id).filter((T)=>T!=null))];if(!K.length){for(let T of e)T[c]=[];continue}let L=await A.selectFrom(D).whereIn(C,K).get(),S=new Map;for(let T of L??[]){let E=String(T[C]),te=S.get(E)??[];te.push(G(T)),S.set(E,te)}for(let T of e)T[c]=S.get(String(T.id))??[]}}return e}async function Je(e,t){let n=t?.set;if(!n||typeof n!=="object")return e;let r={...e};for(let[s,o]of Object.entries(n)){if(typeof o!=="function"||!(s in r))continue;try{r[s]=await o(r)}catch{}}return r}function qe(e,t){return me(e,t?.casts,"set")}function se(e,t){return me(e,t?.casts,"get")}function d(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":"application/json"}})}var le=1048576;async function ke(e){if(Number(e.headers?.get?.("content-length")??0)>le)throw new B(413,`Request body exceeds ${le}-byte limit`);if(e.jsonBody&&typeof e.jsonBody==="object")return e.jsonBody;if(e.formBody&&typeof e.formBody==="object")return e.formBody;try{let n=await e.clone().text();if(n.length>le)throw new B(413,`Request body exceeds ${le}-byte limit`);if(!n)return{};let r=JSON.parse(n);return r&&typeof r==="object"&&!Array.isArray(r)?r:{}}catch(n){if(n instanceof B)throw n;throw new B(400,`Invalid JSON body: ${n.message}`)}}function ce(e){if(e==null)return null;if(typeof e==="number")return Number.isFinite(e)&&e>0?e:null;if(typeof e==="string"){let t=e.trim();if(!t)return null;if(/^\d+$/.test(t)){let n=Number(t);return Number.isFinite(n)&&n>0?n:null}return t}return null}function mt(e){let t=e.bearerToken;if(typeof t==="function"){let r=t.call(e);if(r)return r}let n=e.headers?.get?.("authorization")||e.headers?.get?.("Authorization")||"";if(typeof n==="string"&&n.startsWith("Bearer "))return n.substring(7);return null}async function Z(e){let t=e._authenticatedUser;if(t)return t;let n=mt(e);if(!n)return null;try{let{Auth:r}=await import("@stacksjs/auth"),s=await r.getUserFromToken(n);if(s)e._authenticatedUser=s;return s||null}catch{return null}}function re(e,t){if(e==null||t==null)return!1;if(Array.isArray(t))return t.some((n)=>String(n)===String(e));return String(e)===String(t)}async function ee(e,t,n){let r=ue(e);if(!r||!r.field||typeof r.resolve!=="function")return{enforced:!1,value:null,field:"",bypass:!1};let s=typeof r.bypass==="function"?!!r.bypass(t,n):!1,o=null;try{o=await r.resolve(t,n)}catch{o=null}return{enforced:!0,value:o,field:String(r.field),bypass:s}}async function Ve(e,t,n,r){let s=e?.authedFill?.[t];if(!s)return{};if(typeof s==="function")try{let o=await s(n,r);return o&&typeof o==="object"?o:{}}catch{return{}}if(typeof s==="object"){let o={};for(let[a,l]of Object.entries(s))try{o[a]=typeof l==="function"?await l(n,r):l}catch{}return o}return{}}var Ke=await(async()=>{let e=dt.env.STACKS_API_ROW_SCOPING;try{let t=(await import(Se("config/security.ts"))).default;return be(t?.api?.rowScoping,e)}catch{return be(void 0,e)}})(),Xe=[];for(let[e,t]of Object.entries(_e)){let n=t.traits?.useApi;if(!n)continue;let r=typeof n==="object"?n:{},s=r.uri||t.table||e.toLowerCase()+"s",o=r.routes||["index","show","store","update","destroy"],a=t.table||s,l=Ne(t),w=Ge(t),c=Ae(s,r.prefix),P=ve(t.attributes,w),{read:v,write:D,declared:Q}=je(n),G=["store","update","destroy"].some((p)=>o.includes(p));if(G&&Q&&D.length===0)V.warn(`[orm] ${e}: registering UNAUTHENTICATED mutating routes at ${c} (explicit \`middleware: []\` opt-out)`);if(["index","show"].some((p)=>o.includes(p))&&Q&&v.length===0)V.debug(`[orm] ${e}: registering public read routes at ${c} (declared \`middleware.read: []\`)`);let C=!!ue(t),K=C&&!v.includes("auth")?["auth",...v]:v,L=$e(t),S=G&&!C&&!L;if(S)Xe.push(e);if(G&&L)V.debug(`[orm] ${e}: registering unscoped mutating routes at ${c} (declared \`ownership: false\`)`);let T=S&&Ke==="deny",E=(p,i=K)=>{let u=p;for(let f of i)if(u&&typeof u.middleware==="function")u=u.middleware(f);return u};if(o.includes("index")&&!q("GET",c))E(U.get(c,async(p)=>{try{let i=new URL(p.url),{page:u,perPage:f,offset:h}=Me(i.searchParams),m=i.searchParams.get("sort"),g=A.selectFrom(a);g=Ce(g,m,P);let N=new Set(["page","per_page","sort","fields","search","include","limit","offset","with_count","withTrashed","onlyTrashed"]);for(let[x,O]of i.searchParams.entries()){if(N.has(x)||!O)continue;if(!/^[a-z_][a-z0-9_]*$/i.test(x))continue;let z=P.get(x);if(z)g=g.where(z,"=",O)}let R=i.searchParams.get("search");if(R&&l.length>0){let x=l.slice(0,5);g=g.where((O)=>{for(let z of x)O=O.orWhere(_(z),"like",`%${R}%`);return O})}let k=i.searchParams.get("fields");if(k){let x=k.split(",").map((O)=>O.trim()).filter((O)=>/^[a-z_][a-z0-9_]*$/i.test(O)).map((O)=>_(O));if(x.length>0)g=g.select(x)}let I=!!t.traits?.useSoftDeletes,b=i.searchParams.get("withTrashed")==="true",j=i.searchParams.get("onlyTrashed")==="true";if(I&&!b)g=j?g.whereNotNull("deleted_at"):g.whereNull("deleted_at");let y=C?await ee(t,await Z(p),p):{enforced:!1,value:null,field:"",bypass:!1};if(y.enforced&&!y.bypass){if(y.value==null||Array.isArray(y.value)&&y.value.length===0){let x=he(i,u,f,0,!1,void 0);return new Response(JSON.stringify({data:[],...x,meta:ae(i,u,f,0,!1,void 0)}),{status:200,headers:{"Content-Type":"application/json","Cache-Control":"private, no-store",Vary:"Authorization"}})}g=Array.isArray(y.value)?g.where(y.field,"in",y.value):g.where(y.field,"=",y.value)}let M=await g.limit(f+1).offset(h).get()||[],F=M.length>f,ne=(F?M.slice(0,f):M).map((x)=>Y(se(x,t),w)),xe=i.searchParams.get("include");if(xe)ne=await He(ne,t,xe,_e);let Ye=i.searchParams.get("with_count")==="true",X;if(Ye)try{let x=A.selectFrom(a);if(y.enforced&&!y.bypass&&y.value!=null)x=Array.isArray(y.value)?x.where(y.field,"in",y.value):x.where(y.field,"=",y.value);let O=await x.count(),z=typeof O==="number"?O:Number(O?.count??O);if(Number.isFinite(z))X=z}catch{}let oe={"Content-Type":"application/json"};if(X!==void 0&&!Number.isNaN(X))oe["X-Total-Count"]=String(X);oe["Cache-Control"]=y.enforced&&!y.bypass?"private, no-store":"public, max-age=15, must-revalidate",oe.Vary="Authorization";let Ze=he(i,u,f,ne.length,F,X);return new Response(JSON.stringify({data:ne,...Ze,meta:ae(i,u,f,ne.length,F,X)}),{status:200,headers:oe})}catch(i){if(i instanceof B){let u={error:i.message};if(i.details!==void 0)u.details=i.details;return d(u,i.status||400)}return d({error:`Failed to fetch ${s}`,detail:String(i)},500)}}));if(o.includes("show")&&!q("GET",`${c}/{id}`))E(U.get(`${c}/{id}`,async(p)=>{try{let i=ce(p.params?.id);if(i==null)return d({error:"Invalid ID parameter"},400);let u=await A.selectFrom(a).where({id:i}).executeTakeFirst();if(!u)return d({error:`${e} not found`},404);let f=new URL(p.url);if(t.traits?.useSoftDeletes&&u.deleted_at&&f.searchParams.get("withTrashed")!=="true")return d({error:`${e} not found`},404);let h=C?await ee(t,await Z(p),p):{enforced:!1,value:null,field:"",bypass:!1};if(h.enforced&&!h.bypass){if(h.value==null||!re(u[h.field],h.value))return d({error:`${e} not found`},404)}let m=Y(se(u,t),w),g=f.searchParams.get("include");if(g){let[b]=await He([m],t,g,_e);m=b}let N=m?.updated_at||m?.created_at,R=N?`W/"${m.id}-${String(N)}"`:void 0,k=p.headers?.get?.("if-none-match");if(R&&k&&k===R)return new Response(null,{status:304,headers:{ETag:R}});let I={"Content-Type":"application/json"};if(R)I.ETag=R;return I["Cache-Control"]=h.enforced&&!h.bypass?"private, no-store":"public, max-age=30, must-revalidate",I.Vary="Authorization",new Response(JSON.stringify({data:m}),{status:200,headers:I})}catch(i){if(i instanceof B){let u={error:i.message};if(i.details!==void 0)u.details=i.details;return d(u,i.status||400)}return d({error:`Failed to fetch ${e}`,detail:String(i)},500)}}));if(!T&&o.includes("store")&&!q("POST",c))E(U.post(c,async(p)=>{try{let i=await ke(p),u=ye(i,w),f=pe(u,l),h=await Z(p);if(h){let b=await Ve(t,"creating",h,p);for(let[j,y]of Object.entries(b))if(y!==void 0&&y!==null&&f[j]===void 0)f[j]=y}let m=await ee(t,h,p);if(m.enforced&&!m.bypass){if(!h)return d({error:"Auth required"},401);let b=Ie(f,m.field,m.value);if(b.error)return d({error:b.error},403);f=b.data}if(Object.keys(f).length===0)return d({error:"No fillable fields provided"},422);let g=we(f,t,"creating");if(!g.valid)return d({success:!1,message:"Validation failed",errors:g.errors},422);if(t.traits?.useTimestamps!==!1){let b=new Date().toISOString();f.created_at=b,f.updated_at=b}let N=await Je(f,t),R=ge(qe(N,t)),k=await A.insertInto(a).values(R).execute(),I=R;try{let b=k?.lastInsertRowid??k?.insertId??k;if(b){let j=await A.selectFrom(a).where({id:b}).executeTakeFirst();if(j)I=j}}catch{}return d({data:Y(se(I,t),w)},201)}catch(i){let{status:u,body:f}=fe(i,e,"create");return d(f,u)}}),D);if(!T&&o.includes("update")){let p=async(i)=>{try{let u=ce(i.params?.id);if(u==null)return d({error:"Invalid ID parameter"},400);let f=await ke(i),h=ye(f,w),m=pe(h,l),g=await Z(i);if(g){let y=await Ve(t,"updating",g,i);for(let[M,F]of Object.entries(y))if(F!==void 0&&F!==null&&m[M]===void 0)m[M]=F}if(Object.keys(m).length===0)return d({error:"No fillable fields provided"},422);let N=we(m,t,"updating");if(!N.valid)return d({success:!1,message:"Validation failed",errors:N.errors},422);let R=await A.selectFrom(a).where({id:u}).executeTakeFirst();if(!R)return d({error:`${e} not found`},404);let k=await ee(t,g,i);if(k.enforced&&!k.bypass){if(!g)return d({error:"Auth required"},401);let y=R[k.field];if(!re(y,k.value))return d({error:`Not your ${e}`},403);let M=Object.keys(m).find((F)=>_(F)===_(k.field));if(M!==void 0&&!re(m[M],k.value))return d({error:`Cannot reassign ${e} ownership`},403)}if(t.traits?.useTimestamps!==!1)m.updated_at=new Date().toISOString();let I=await Je(m,t),b=ge(qe(I,t));await A.updateTable(a).set(b).where({id:u}).execute();let j={...R,...b};try{let y=await A.selectFrom(a).where({id:u}).executeTakeFirst();if(y)j=y}catch{}return d({data:Y(se(j,t),w)})}catch(u){let{status:f,body:h}=fe(u,e,"update");return d(h,f)}};if(!q("PUT",`${c}/{id}`))E(U.put(`${c}/{id}`,p),D);if(!q("PATCH",`${c}/{id}`))E(U.patch(`${c}/{id}`,p),D)}let te=!!t.traits?.useSoftDeletes;if(!T&&o.includes("destroy")&&!q("DELETE",`${c}/{id}`))E(U.delete(`${c}/{id}`,async(p)=>{try{let i=ce(p.params?.id);if(i==null)return d({error:"Invalid ID parameter"},400);let u=await A.selectFrom(a).where({id:i}).executeTakeFirst();if(!u)return d({error:`${e} not found`},404);let f=await Z(p),h=await ee(t,f,p);if(h.enforced&&!h.bypass){if(!f)return d({error:"Auth required"},401);let m=u[h.field];if(!re(m,h.value))return d({error:`Not your ${e}`},403)}if(te){let m=new Date().toISOString();await A.updateTable(a).set({deleted_at:m,updated_at:m}).where({id:i}).execute()}else await A.deleteFrom(a).where({id:i}).execute();return new Response(null,{status:204})}catch(i){if(i instanceof B){let u={error:i.message};if(i.details!==void 0)u.details=i.details;return d(u,i.status||400)}return d({error:`Failed to delete ${e}`,detail:String(i)},500)}}),D);if(!T&&o.includes("destroy")&&!q("POST",`${c}/bulk-delete`))E(U.post(`${c}/bulk-delete`,async(p)=>{try{let u=(await ke(p))?.ids;if(!Array.isArray(u)||u.length===0)return d({error:"An array of IDs is required"},422);if(u.length>100)return d({error:"Cannot delete more than 100 records at once"},422);let f=[],h=[];for(let R of u){let k=ce(R);if(k!=null)f.push(k);else h.push(R)}if(h.length>0)return d({error:"Invalid IDs in batch",invalid:h},422);let m=await Z(p),g=await ee(t,m,p);if(g.enforced&&!g.bypass){if(!m)return d({error:"Auth required"},401);if(g.value==null)return d({error:"Caller has no ownership identity"},403);let R=await A.selectFrom(a).select(["id",g.field]).execute(),k=new Set(R.filter((b)=>re(b[g.field],g.value)).map((b)=>String(b.id))),I=f.filter((b)=>!k.has(String(b)));if(I.length>0)return d({error:`Cannot delete ${e} you don't own`,ids:I},403)}let N=new Date().toISOString();for(let R of f)if(te)await A.updateTable(a).set({deleted_at:N,updated_at:N}).where({id:R}).execute();else await A.deleteFrom(a).where({id:R}).execute();return d({message:`Successfully deleted ${f.length} ${s}`})}catch(i){if(i instanceof B){let u={error:i.message};if(i.details!==void 0)u.details=i.details;return d(u,i.status||400)}return d({error:`Failed to bulk delete ${s}`,detail:String(i)},500)}}),D)}{let e=Fe(Xe,Ke);if(e)V.warn(e)}var Qt=U;export{Qt as default};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/orm",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.1",
5
+ "version": "0.74.3",
6
6
  "description": "The Stacks ORM integration",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -63,26 +63,26 @@
63
63
  "prepublishOnly": "bun run build"
64
64
  },
65
65
  "dependencies": {
66
- "@stacksjs/auth": "0.74.1",
67
- "@stacksjs/cache": "0.74.1",
68
- "@stacksjs/cli": "0.74.1",
69
- "@stacksjs/config": "0.74.1",
70
- "@stacksjs/database": "0.74.1",
71
- "@stacksjs/env": "0.74.1",
72
- "@stacksjs/error-handling": "0.74.1",
73
- "@stacksjs/events": "0.74.1",
74
- "@stacksjs/logging": "0.74.1",
75
- "@stacksjs/path": "0.74.1",
76
- "@stacksjs/payments": "0.74.1",
77
- "@stacksjs/query-builder": "0.74.1",
78
- "@stacksjs/queue": "0.74.1",
79
- "@stacksjs/realtime": "0.74.1",
80
- "@stacksjs/router": "0.74.1",
81
- "@stacksjs/search-engine": "0.74.1",
82
- "@stacksjs/storage": "0.74.1",
83
- "@stacksjs/strings": "0.74.1",
66
+ "@stacksjs/auth": "0.74.3",
67
+ "@stacksjs/cache": "0.74.3",
68
+ "@stacksjs/cli": "0.74.3",
69
+ "@stacksjs/config": "0.74.3",
70
+ "@stacksjs/database": "0.74.3",
71
+ "@stacksjs/env": "0.74.3",
72
+ "@stacksjs/error-handling": "0.74.3",
73
+ "@stacksjs/events": "0.74.3",
74
+ "@stacksjs/logging": "0.74.3",
75
+ "@stacksjs/path": "0.74.3",
76
+ "@stacksjs/payments": "0.74.3",
77
+ "@stacksjs/query-builder": "0.74.3",
78
+ "@stacksjs/queue": "0.74.3",
79
+ "@stacksjs/realtime": "0.74.3",
80
+ "@stacksjs/router": "0.74.3",
81
+ "@stacksjs/search-engine": "0.74.3",
82
+ "@stacksjs/storage": "0.74.3",
83
+ "@stacksjs/strings": "0.74.3",
84
84
  "@stacksjs/ts-validation": "^0.5.6",
85
- "@stacksjs/validation": "0.74.1",
85
+ "@stacksjs/validation": "0.74.3",
86
86
  "bun-query-builder": "^0.2.53"
87
87
  },
88
88
  "peerDependencies": {
@@ -94,7 +94,7 @@
94
94
  }
95
95
  },
96
96
  "devDependencies": {
97
- "@stacksjs/types": "0.74.1",
97
+ "@stacksjs/types": "0.74.3",
98
98
  "better-dx": "^0.2.24",
99
99
  "stripe": "22.3.2"
100
100
  }