@stacksjs/orm 0.75.247 → 0.75.249
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/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"@stacksjs/validation/runtime";import{isCompiledBinary}from"@stacksjs/path";export*from"./utils/prunable";export{collectEncryptedAttributes,decryptValue,encryptValue,isEncrypted}from"./utils/encrypted";export{setAuditUser,createAuditMethods}from"./traits/audit";export{createCommentableMethods}from"./traits/commentable";export{createTaggableMethods}from"./traits/taggable";export{createCategorizableMethods}from"./traits/categorizable";export{createLikeableMethods}from"./traits/likeable";export{BILLABLE_INSTANCE_METHODS,isBillable,SubscriptionNotOwnedError}from"./traits/billable";export{filterFillable,getWritableFields,isUniqueViolation,mapWriteError,toSnakeCaseKeys}from"./auto-crud";export{customerOwnership,parentOwnership,selfOwnership,siteOwnership,teamMembershipOwnership}from"./ownership";export*from"./batch-loader";export*from"./db";export*from"./subquery";export*from"./transaction";export*from"./locked-row";export*from"./row-token";export*from"./gdpr";export*from"./model-types";export*from"./types";export*from"@stacksjs/model-meta";export*from"./define-model";export*from"./extend-model";export{buildDatabaseSchema,renderDatabaseTypeFile}from"./generate-database-schema";export{isCursorPaginator,isPaginator,isSimplePaginator,toCursorPaginator,toPaginator,toSimplePaginator}from"@stacksjs/pagination";export{enrichPaginatorUrls,parseCursor,resolveCursorArgs,resolvePageArgs}from"./paginator-request";import{setConfig}from"@stacksjs/query-builder";function autoConfigureOrm(){try{const dbPath=process.env.DB_DATABASE_PATH||"database/stacks.sqlite";if((process.env.DB_CONNECTION||"sqlite")==="sqlite")setConfig({dialect:"sqlite",database:{database:dbPath}})}catch{}}autoConfigureOrm();async function importDefault(modulePath){for(let attempt=0;;attempt++){const mod=await import(modulePath);try{return mod.default}catch(err){if(attempt<5&&err instanceof ReferenceError&&/before initialization/.test(err.message)){await new Promise((resolve)=>setTimeout(resolve,0));continue}throw err}}}let announcedCompiledSkip=!1;async function loadUserlandModel(modelName,subdirs=[""]){if(isCompiledBinary()){if(!announcedCompiledSkip){announcedCompiledSkip=!0;console.debug("[orm] running from a compiled binary, so framework default models are not probed on disk; models this app uses are bundled.")}return null}const{path}=await import("@stacksjs/path"),userPath=path.userModelsPath(`${modelName}.ts`);try{if(await Bun.file(userPath).exists())return await importDefault(userPath)}catch(err){console.error(`[orm] Failed to import userland model ${modelName} from ${userPath} - falling back to framework default. Reason:`,err)}for(const sub of subdirs){const prefix=sub?`${sub}/`:"",defaultPath=path.frameworkPath(`defaults/app/Models/${prefix}${modelName}.ts`);try{if(await Bun.file(defaultPath).exists())return await importDefault(defaultPath)}catch(err){console.error(`[orm] Failed to import framework default model ${modelName} from ${defaultPath} - trying next subdir. Reason:`,err)}}return null}const _loaded={};let _modelsSettled=!1;const QUERY_ENTRYPOINTS=new Set(["where","orWhere","whereIn","whereNotIn","whereNull","whereNotNull","whereLike","whereBetween","whereNotBetween","orderBy","orderByDesc","latest","oldest","limit","take","skip","select","find","findOrFail","findMany","first","firstOrFail","all","get","paginate","count","sum","avg","min","max","create","createMany","firstOrCreate","updateOrCreate","save","update","updateMany","delete","destroy","remove","truncate"]);function featureGateFor(name){return FRAMEWORK_MODEL_MANIFEST.find(([model])=>model===name)?.[2]}function unavailableModel(name){const gate=featureGateFor(name),because=gate?`The '${gate}' feature is off, so this model was never loaded - set \`enabled: true\` in config/${gate}.ts to use it.`:"No model file for it was found under app/Models or the framework defaults.";return Error(`Model "${name}" is not available in this process. ${because}`)}let _readyResolve=()=>{};export const ormReady=new Promise((resolve)=>{_readyResolve=resolve});function lazyModel(name){return new Proxy(function _modelStub(){},{get(_target,prop){if(prop==="then")return;const m=_loaded[name];if(m==null){if(_modelsSettled&&typeof prop==="string"&&QUERY_ENTRYPOINTS.has(prop))throw unavailableModel(name);return}const value=m[prop];return typeof value==="function"?value.bind(m):value},set(_target,prop,value){const m=_loaded[name];if(m==null)return!1;m[prop]=value;return!0},has(_target,prop){const m=_loaded[name];return m==null?!1:(prop in m)},apply(_target,thisArg,args){const m=_loaded[name];if(m==null||typeof m!=="function")return;return m.apply(thisArg,args)}})}export const User=lazyModel("User"),Job=lazyModel("Job"),FailedJob=lazyModel("FailedJob");const FRAMEWORK_MODEL_MANIFEST=[["Referral",[""],"auth"],["ReferralCode",[""],"auth"],["Team",[""],"auth"],["TeamInvitation",[""],"auth"],["TeamMember",[""],"auth"],["SocialAccount",[""],"auth"],["Subscriber",[""],"auth"],["SubscriberEmail",[""],"auth"],["Subscription",[""],"auth"],["Categorizable",["Content"],"cms"],["Post",["Content"],"cms"],["Page",["Content"],"cms"],["PageRevision",["Content"],"cms"],["Redirect",["Content"],"cms"],["Menu",["Content"],"cms"],["MenuItem",["Content"],"cms"],["Author",["Content"],"cms"],["Comment",[""],"cms"],["Tag",[""],"cms"],["Site",[""],"auth"],["SiteDomain",[""],"auth"],["MagicLinkToken",[""],"auth"],["SmsOptOut",[""],"auth"],["GdprRequest",[""],"auth"],["Form",["Forms"],"forms"],["FormField",["Forms"],"forms"],["FormSubmission",["Forms"],"forms"],["Activity",[""],"dashboard"],["Board",[""],"dashboard"],["BoardColumn",[""],"dashboard"],["Card",[""],"dashboard"],["CardAttachment",[""],"dashboard"],["CardComment",[""],"dashboard"],["Deployment",[""],"dashboard"],["FeedbackToken",[""],"dashboard"],["Label",[""],"dashboard"],["Release",[""],"dashboard"],["Notification",[""],"dashboard"],["NotificationDelivery",[""],"dashboard"],["PersonalAccessToken",[""],"dashboard"],["MailPreference",[""],"dashboard"],["Log",[""],"dashboard"],["Request",[""],"dashboard"],["AnalyticsEvent",[""],"dashboard"],["QueryLog",[""],"dashboard"],["StorageItem",[""],"dashboard"],["StorageItemTask",[""],"dashboard"],["Error",[""],"monitoring"],["Websocket",["realtime"],"realtime"],["Campaign",[""],"marketing"],["CampaignSend",[""],"marketing"],["EmailList",[""],"marketing"],["EmailListSubscriber",[""],"marketing"],["EmailIdempotency",[""],"marketing"],["EmailSuppression",[""],"marketing"],["EmailWebhookEvent",[""],"marketing"],["SocialPost",[""],"marketing"],["Automation",[""],"marketing"],["AutomationRun",[""],"marketing"],["CampaignVariant",[""],"marketing"],["CommunicationSuppression",[""],"marketing"],["ConsentEvent",[""],"marketing"],["SenderDomain",[""],"marketing"],["UsageEvent",[""],"marketing"],["PaymentMethod",[""],"commerce"],["PaymentProduct",[""],"commerce"],["PaymentTransaction",[""],"commerce"],["Order",["commerce"],"commerce"],["OrderIdempotency",["commerce"],"commerce"],["OrderItem",["commerce"],"commerce"],["Cart",["commerce"],"commerce"],["CartItem",["commerce"],"commerce"],["Customer",["commerce"],"commerce"],["Product",["commerce"],"commerce"],["ProductVariant",["commerce"],"commerce"],["ProductUnit",["commerce"],"commerce"],["Manufacturer",["commerce"],"commerce"],["Category",["commerce"],"commerce"],["Coupon",["commerce"],"commerce"],["GiftCard",["commerce"],"commerce"],["LicenseKey",["commerce"],"commerce"],["Review",["commerce"],"commerce"],["Receipt",["commerce"],"commerce"],["PrintDevice",["commerce"],"commerce"],["ShippingMethod",["commerce"],"commerce"],["ShippingRate",["commerce"],"commerce"],["ShippingZone",["commerce"],"commerce"],["DeliveryRoute",["commerce"],"commerce"],["DeliveryStop",["commerce"],"commerce"],["DigitalDelivery",["commerce"],"commerce"],["Courier",["commerce"],"commerce"],["CourierPing",["commerce"],"commerce"],["TaxRate",["commerce"],"commerce"],["WaitlistProduct",["commerce"],"commerce"],["WaitlistRestaurant",["commerce"],"commerce"],["Auction",["commerce"],"commerce"],["AuctionItem",["commerce"],"commerce"],["Bid",["commerce"],"commerce"],["Pledge",["commerce"],"commerce"],["LoyaltyPoint",["commerce"],"commerce"],["LoyaltyReward",["commerce"],"commerce"],["Payment",["commerce"],"commerce"],["PaymentWebhookEvent",["commerce"],"commerce"],["Transaction",["commerce"],"commerce"]];queueMicrotask(async()=>{try{const{feature}=await import("@stacksjs/config"),userModel=await loadUserlandModel("User");if(userModel)_loaded.User=userModel;if(feature("queue")){const job=await loadUserlandModel("Job");if(job)_loaded.Job=job;const failedJob=await loadUserlandModel("FailedJob");if(failedJob)_loaded.FailedJob=failedJob}for(const[name,subdirs,featureFlag]of FRAMEWORK_MODEL_MANIFEST){if(!feature(featureFlag))continue;const M=await loadUserlandModel(name,subdirs);if(M)_loaded[name]=M}}catch(err){console.error("[orm] deferred model load failed:",err)}finally{_modelsSettled=!0;_readyResolve()}});export const Activity=lazyModel("Activity"),AnalyticsEvent=lazyModel("AnalyticsEvent"),Automation=lazyModel("Automation"),AutomationRun=lazyModel("AutomationRun"),Auction=lazyModel("Auction"),CampaignVariant=lazyModel("CampaignVariant"),CommunicationSuppression=lazyModel("CommunicationSuppression"),ConsentEvent=lazyModel("ConsentEvent"),SenderDomain=lazyModel("SenderDomain"),UsageEvent=lazyModel("UsageEvent"),AuctionItem=lazyModel("AuctionItem"),Author=lazyModel("Author"),Bid=lazyModel("Bid"),Board=lazyModel("Board"),BoardColumn=lazyModel("BoardColumn"),Campaign=lazyModel("Campaign"),Card=lazyModel("Card"),CardAttachment=lazyModel("CardAttachment"),CardComment=lazyModel("CardComment"),Cart=lazyModel("Cart"),CartItem=lazyModel("CartItem"),Category=lazyModel("Category"),Comment=lazyModel("Comment"),Coupon=lazyModel("Coupon"),Customer=lazyModel("Customer"),DeliveryRoute=lazyModel("DeliveryRoute"),DeliveryStop=lazyModel("DeliveryStop"),Deployment=lazyModel("Deployment"),FeedbackToken=lazyModel("FeedbackToken"),DigitalDelivery=lazyModel("DigitalDelivery"),Courier=lazyModel("Courier"),CourierPing=lazyModel("CourierPing"),CampaignSend=lazyModel("CampaignSend"),EmailList=lazyModel("EmailList"),EmailListSubscriber=lazyModel("EmailListSubscriber"),Form=lazyModel("Form"),FormField=lazyModel("FormField"),FormSubmission=lazyModel("FormSubmission"),MagicLinkToken=lazyModel("MagicLinkToken"),Menu=lazyModel("Menu"),MenuItem=lazyModel("MenuItem"),PageRevision=lazyModel("PageRevision"),Pledge=lazyModel("Pledge"),Redirect=lazyModel("Redirect"),Site=lazyModel("Site"),SiteDomain=lazyModel("SiteDomain"),SmsOptOut=lazyModel("SmsOptOut"),GdprRequest=lazyModel("GdprRequest"),StorageItem=lazyModel("StorageItem"),StorageItemTask=lazyModel("StorageItemTask"),EmailIdempotency=lazyModel("EmailIdempotency"),EmailSuppression=lazyModel("EmailSuppression"),EmailWebhookEvent=lazyModel("EmailWebhookEvent"),ErrorModel=lazyModel("Error"),GiftCard=lazyModel("GiftCard"),Label=lazyModel("Label"),LicenseKey=lazyModel("LicenseKey"),Log=lazyModel("Log"),LoyaltyPoint=lazyModel("LoyaltyPoint"),LoyaltyReward=lazyModel("LoyaltyReward"),MailPreference=lazyModel("MailPreference"),Manufacturer=lazyModel("Manufacturer"),Notification=lazyModel("Notification"),NotificationDelivery=lazyModel("NotificationDelivery"),Order=lazyModel("Order"),OrderIdempotency=lazyModel("OrderIdempotency"),PersonalAccessToken=lazyModel("PersonalAccessToken"),OrderItem=lazyModel("OrderItem"),Page=lazyModel("Page"),Payment=lazyModel("Payment"),PaymentWebhookEvent=lazyModel("PaymentWebhookEvent"),PaymentMethod=lazyModel("PaymentMethod"),PaymentProduct=lazyModel("PaymentProduct"),PaymentTransaction=lazyModel("PaymentTransaction"),Post=lazyModel("Post"),Categorizable=lazyModel("Categorizable"),PrintDevice=lazyModel("PrintDevice"),Product=lazyModel("Product"),ProductUnit=lazyModel("ProductUnit"),ProductVariant=lazyModel("ProductVariant"),QueryLog=lazyModel("QueryLog"),Receipt=lazyModel("Receipt"),Referral=lazyModel("Referral"),ReferralCode=lazyModel("ReferralCode"),Release=lazyModel("Release"),Request=lazyModel("Request"),Review=lazyModel("Review"),ShippingMethod=lazyModel("ShippingMethod"),ShippingRate=lazyModel("ShippingRate"),ShippingZone=lazyModel("ShippingZone"),SocialPost=lazyModel("SocialPost"),SocialAccount=lazyModel("SocialAccount"),Subscriber=lazyModel("Subscriber"),SubscriberEmail=lazyModel("SubscriberEmail"),Subscription=lazyModel("Subscription"),Tag=lazyModel("Tag"),TaxRate=lazyModel("TaxRate"),Team=lazyModel("Team"),TeamInvitation=lazyModel("TeamInvitation"),TeamMember=lazyModel("TeamMember"),Transaction=lazyModel("Transaction"),WaitlistProduct=lazyModel("WaitlistProduct"),WaitlistRestaurant=lazyModel("WaitlistRestaurant"),Websocket=lazyModel("Websocket");const _allExports={User,Job,FailedJob,Activity,AnalyticsEvent,Automation,AutomationRun,Auction,AuctionItem,Author,Bid,Board,BoardColumn,Campaign,CampaignSend,CampaignVariant,Card,CardComment,Cart,CartItem,Categorizable,Category,Comment,Coupon,Customer,CommunicationSuppression,ConsentEvent,DeliveryRoute,DeliveryStop,Deployment,DigitalDelivery,Courier,CourierPing,EmailList,EmailListSubscriber,EmailIdempotency,EmailSuppression,EmailWebhookEvent,ErrorModel,Form,FormField,FormSubmission,GdprRequest,GiftCard,Label,LicenseKey,Log,LoyaltyPoint,LoyaltyReward,MailPreference,Manufacturer,Notification,MagicLinkToken,Menu,MenuItem,NotificationDelivery,Order,OrderIdempotency,OrderItem,Page,PageRevision,Payment,PaymentMethod,PaymentProduct,PaymentTransaction,PaymentWebhookEvent,Pledge,Post,PrintDevice,Product,ProductUnit,ProductVariant,QueryLog,Receipt,Release,Request,Review,ShippingMethod,Redirect,SenderDomain,ShippingRate,ShippingZone,Site,SiteDomain,SmsOptOut,SocialAccount,SocialPost,Subscriber,SubscriberEmail,Subscription,Tag,TaxRate,Team,TeamInvitation,TeamMember,Transaction,UsageEvent,WaitlistProduct,WaitlistRestaurant,Websocket},g=globalThis;for(const[name,M]of Object.entries(_allExports))if(M&&g[name]===void 0)g[name]=M;export const modelGlobalNames=Object.keys(_allExports);async function _safeAll(M){if(!M)return[];try{if(typeof M.all==="function"){const rows=await M.all();return Array.isArray(rows)?rows:[]}if(typeof M.get==="function"){const rows=await M.get();return Array.isArray(rows)?rows:[]}}catch{}return[]}function _safeGet(row,key,fallback=""){if(!row)return fallback;try{if(typeof row.get==="function"){const v=row.get(key);if(v!==void 0&&v!==null)return v}}catch{}const direct=row?.[key];return direct!==void 0&&direct!==null?direct:fallback}if(g.safeAll===void 0)g.safeAll=_safeAll;if(g.safeGet===void 0)g.safeGet=_safeGet;export*from"./messaging";
|
|
1
|
+
import"@stacksjs/validation/runtime";import{isCompiledBinary}from"@stacksjs/path";export*from"./utils/prunable";export{collectEncryptedAttributes,decryptValue,encryptValue,isEncrypted}from"./utils/encrypted";export{setAuditUser,createAuditMethods}from"./traits/audit";export{createCommentableMethods}from"./traits/commentable";export{createTaggableMethods}from"./traits/taggable";export{createCategorizableMethods}from"./traits/categorizable";export{createLikeableMethods}from"./traits/likeable";export{BILLABLE_INSTANCE_METHODS,isBillable,SubscriptionNotOwnedError}from"./traits/billable";export{filterFillable,getWritableFields,isUniqueViolation,mapWriteError,toSnakeCaseKeys}from"./auto-crud";export{customerOwnership,parentOwnership,selfOwnership,siteOwnership,teamMembershipOwnership}from"./ownership";export*from"./batch-loader";export*from"./db";export*from"./subquery";export*from"./transaction";export*from"./locked-row";export*from"./row-token";export*from"./gdpr";export*from"./model-types";export*from"./types";export*from"@stacksjs/model-meta";export*from"./define-model";export*from"./extend-model";export{buildDatabaseSchema,renderDatabaseTypeFile}from"./generate-database-schema";export{isCursorPaginator,isPaginator,isSimplePaginator,toCursorPaginator,toPaginator,toSimplePaginator}from"@stacksjs/pagination";export{enrichPaginatorUrls,parseCursor,resolveCursorArgs,resolvePageArgs}from"./paginator-request";async function importDefault(modulePath){for(let attempt=0;;attempt++){const mod=await import(modulePath);try{return mod.default}catch(err){if(attempt<5&&err instanceof ReferenceError&&/before initialization/.test(err.message)){await new Promise((resolve)=>setTimeout(resolve,0));continue}throw err}}}let announcedCompiledSkip=!1;async function loadUserlandModel(modelName,subdirs=[""]){if(isCompiledBinary()){if(!announcedCompiledSkip){announcedCompiledSkip=!0;console.debug("[orm] running from a compiled binary, so framework default models are not probed on disk; models this app uses are bundled.")}return null}const{path}=await import("@stacksjs/path"),userPath=path.userModelsPath(`${modelName}.ts`);try{if(await Bun.file(userPath).exists())return await importDefault(userPath)}catch(err){console.error(`[orm] Failed to import userland model ${modelName} from ${userPath} - falling back to framework default. Reason:`,err)}for(const sub of subdirs){const prefix=sub?`${sub}/`:"",defaultPath=path.frameworkPath(`defaults/app/Models/${prefix}${modelName}.ts`);try{if(await Bun.file(defaultPath).exists())return await importDefault(defaultPath)}catch(err){console.error(`[orm] Failed to import framework default model ${modelName} from ${defaultPath} - trying next subdir. Reason:`,err)}}return null}const _loaded={};let _modelsSettled=!1;const QUERY_ENTRYPOINTS=new Set(["where","orWhere","whereIn","whereNotIn","whereNull","whereNotNull","whereLike","whereBetween","whereNotBetween","orderBy","orderByDesc","latest","oldest","limit","take","skip","select","find","findOrFail","findMany","first","firstOrFail","all","get","paginate","count","sum","avg","min","max","create","createMany","firstOrCreate","updateOrCreate","save","update","updateMany","delete","destroy","remove","truncate"]);function featureGateFor(name){return FRAMEWORK_MODEL_MANIFEST.find(([model])=>model===name)?.[2]}function unavailableModel(name){const gate=featureGateFor(name),because=gate?`The '${gate}' feature is off, so this model was never loaded - set \`enabled: true\` in config/${gate}.ts to use it.`:"No model file for it was found under app/Models or the framework defaults.";return Error(`Model "${name}" is not available in this process. ${because}`)}let _readyResolve=()=>{};export const ormReady=new Promise((resolve)=>{_readyResolve=resolve});function lazyModel(name){return new Proxy(function _modelStub(){},{get(_target,prop){if(prop==="then")return;const m=_loaded[name];if(m==null){if(_modelsSettled&&typeof prop==="string"&&QUERY_ENTRYPOINTS.has(prop))throw unavailableModel(name);return}const value=m[prop];return typeof value==="function"?value.bind(m):value},set(_target,prop,value){const m=_loaded[name];if(m==null)return!1;m[prop]=value;return!0},has(_target,prop){const m=_loaded[name];return m==null?!1:(prop in m)},apply(_target,thisArg,args){const m=_loaded[name];if(m==null||typeof m!=="function")return;return m.apply(thisArg,args)}})}export const User=lazyModel("User"),Job=lazyModel("Job"),FailedJob=lazyModel("FailedJob");const FRAMEWORK_MODEL_MANIFEST=[["Referral",[""],"auth"],["ReferralCode",[""],"auth"],["Team",[""],"auth"],["TeamInvitation",[""],"auth"],["TeamMember",[""],"auth"],["SocialAccount",[""],"auth"],["Subscriber",[""],"auth"],["SubscriberEmail",[""],"auth"],["Subscription",[""],"auth"],["Categorizable",["Content"],"cms"],["Post",["Content"],"cms"],["Page",["Content"],"cms"],["PageRevision",["Content"],"cms"],["Redirect",["Content"],"cms"],["Menu",["Content"],"cms"],["MenuItem",["Content"],"cms"],["Author",["Content"],"cms"],["Comment",[""],"cms"],["Tag",[""],"cms"],["Site",[""],"auth"],["SiteDomain",[""],"auth"],["MagicLinkToken",[""],"auth"],["SmsOptOut",[""],"auth"],["GdprRequest",[""],"auth"],["Form",["Forms"],"forms"],["FormField",["Forms"],"forms"],["FormSubmission",["Forms"],"forms"],["Activity",[""],"dashboard"],["Board",[""],"dashboard"],["BoardColumn",[""],"dashboard"],["Card",[""],"dashboard"],["CardAttachment",[""],"dashboard"],["CardComment",[""],"dashboard"],["Deployment",[""],"dashboard"],["FeedbackToken",[""],"dashboard"],["Label",[""],"dashboard"],["Release",[""],"dashboard"],["Notification",[""],"dashboard"],["NotificationDelivery",[""],"dashboard"],["PersonalAccessToken",[""],"dashboard"],["MailPreference",[""],"dashboard"],["Log",[""],"dashboard"],["Request",[""],"dashboard"],["AnalyticsEvent",[""],"dashboard"],["QueryLog",[""],"dashboard"],["StorageItem",[""],"dashboard"],["StorageItemTask",[""],"dashboard"],["Error",[""],"monitoring"],["Websocket",["realtime"],"realtime"],["Campaign",[""],"marketing"],["CampaignSend",[""],"marketing"],["EmailList",[""],"marketing"],["EmailListSubscriber",[""],"marketing"],["EmailIdempotency",[""],"marketing"],["EmailSuppression",[""],"marketing"],["EmailWebhookEvent",[""],"marketing"],["SocialPost",[""],"marketing"],["Automation",[""],"marketing"],["AutomationRun",[""],"marketing"],["CampaignVariant",[""],"marketing"],["CommunicationSuppression",[""],"marketing"],["ConsentEvent",[""],"marketing"],["SenderDomain",[""],"marketing"],["UsageEvent",[""],"marketing"],["PaymentMethod",[""],"commerce"],["PaymentProduct",[""],"commerce"],["PaymentTransaction",[""],"commerce"],["Order",["commerce"],"commerce"],["OrderIdempotency",["commerce"],"commerce"],["OrderItem",["commerce"],"commerce"],["Cart",["commerce"],"commerce"],["CartItem",["commerce"],"commerce"],["Customer",["commerce"],"commerce"],["Product",["commerce"],"commerce"],["ProductVariant",["commerce"],"commerce"],["ProductUnit",["commerce"],"commerce"],["Manufacturer",["commerce"],"commerce"],["Category",["commerce"],"commerce"],["Coupon",["commerce"],"commerce"],["GiftCard",["commerce"],"commerce"],["LicenseKey",["commerce"],"commerce"],["Review",["commerce"],"commerce"],["Receipt",["commerce"],"commerce"],["PrintDevice",["commerce"],"commerce"],["ShippingMethod",["commerce"],"commerce"],["ShippingRate",["commerce"],"commerce"],["ShippingZone",["commerce"],"commerce"],["DeliveryRoute",["commerce"],"commerce"],["DeliveryStop",["commerce"],"commerce"],["DigitalDelivery",["commerce"],"commerce"],["Courier",["commerce"],"commerce"],["CourierPing",["commerce"],"commerce"],["TaxRate",["commerce"],"commerce"],["WaitlistProduct",["commerce"],"commerce"],["WaitlistRestaurant",["commerce"],"commerce"],["Auction",["commerce"],"commerce"],["AuctionItem",["commerce"],"commerce"],["Bid",["commerce"],"commerce"],["Pledge",["commerce"],"commerce"],["LoyaltyPoint",["commerce"],"commerce"],["LoyaltyReward",["commerce"],"commerce"],["Payment",["commerce"],"commerce"],["PaymentWebhookEvent",["commerce"],"commerce"],["Transaction",["commerce"],"commerce"]];queueMicrotask(async()=>{try{const{feature}=await import("@stacksjs/config"),userModel=await loadUserlandModel("User");if(userModel)_loaded.User=userModel;if(feature("queue")){const job=await loadUserlandModel("Job");if(job)_loaded.Job=job;const failedJob=await loadUserlandModel("FailedJob");if(failedJob)_loaded.FailedJob=failedJob}for(const[name,subdirs,featureFlag]of FRAMEWORK_MODEL_MANIFEST){if(!feature(featureFlag))continue;const M=await loadUserlandModel(name,subdirs);if(M)_loaded[name]=M}}catch(err){console.error("[orm] deferred model load failed:",err)}finally{_modelsSettled=!0;_readyResolve()}});export const Activity=lazyModel("Activity"),AnalyticsEvent=lazyModel("AnalyticsEvent"),Automation=lazyModel("Automation"),AutomationRun=lazyModel("AutomationRun"),Auction=lazyModel("Auction"),CampaignVariant=lazyModel("CampaignVariant"),CommunicationSuppression=lazyModel("CommunicationSuppression"),ConsentEvent=lazyModel("ConsentEvent"),SenderDomain=lazyModel("SenderDomain"),UsageEvent=lazyModel("UsageEvent"),AuctionItem=lazyModel("AuctionItem"),Author=lazyModel("Author"),Bid=lazyModel("Bid"),Board=lazyModel("Board"),BoardColumn=lazyModel("BoardColumn"),Campaign=lazyModel("Campaign"),Card=lazyModel("Card"),CardAttachment=lazyModel("CardAttachment"),CardComment=lazyModel("CardComment"),Cart=lazyModel("Cart"),CartItem=lazyModel("CartItem"),Category=lazyModel("Category"),Comment=lazyModel("Comment"),Coupon=lazyModel("Coupon"),Customer=lazyModel("Customer"),DeliveryRoute=lazyModel("DeliveryRoute"),DeliveryStop=lazyModel("DeliveryStop"),Deployment=lazyModel("Deployment"),FeedbackToken=lazyModel("FeedbackToken"),DigitalDelivery=lazyModel("DigitalDelivery"),Courier=lazyModel("Courier"),CourierPing=lazyModel("CourierPing"),CampaignSend=lazyModel("CampaignSend"),EmailList=lazyModel("EmailList"),EmailListSubscriber=lazyModel("EmailListSubscriber"),Form=lazyModel("Form"),FormField=lazyModel("FormField"),FormSubmission=lazyModel("FormSubmission"),MagicLinkToken=lazyModel("MagicLinkToken"),Menu=lazyModel("Menu"),MenuItem=lazyModel("MenuItem"),PageRevision=lazyModel("PageRevision"),Pledge=lazyModel("Pledge"),Redirect=lazyModel("Redirect"),Site=lazyModel("Site"),SiteDomain=lazyModel("SiteDomain"),SmsOptOut=lazyModel("SmsOptOut"),GdprRequest=lazyModel("GdprRequest"),StorageItem=lazyModel("StorageItem"),StorageItemTask=lazyModel("StorageItemTask"),EmailIdempotency=lazyModel("EmailIdempotency"),EmailSuppression=lazyModel("EmailSuppression"),EmailWebhookEvent=lazyModel("EmailWebhookEvent"),ErrorModel=lazyModel("Error"),GiftCard=lazyModel("GiftCard"),Label=lazyModel("Label"),LicenseKey=lazyModel("LicenseKey"),Log=lazyModel("Log"),LoyaltyPoint=lazyModel("LoyaltyPoint"),LoyaltyReward=lazyModel("LoyaltyReward"),MailPreference=lazyModel("MailPreference"),Manufacturer=lazyModel("Manufacturer"),Notification=lazyModel("Notification"),NotificationDelivery=lazyModel("NotificationDelivery"),Order=lazyModel("Order"),OrderIdempotency=lazyModel("OrderIdempotency"),PersonalAccessToken=lazyModel("PersonalAccessToken"),OrderItem=lazyModel("OrderItem"),Page=lazyModel("Page"),Payment=lazyModel("Payment"),PaymentWebhookEvent=lazyModel("PaymentWebhookEvent"),PaymentMethod=lazyModel("PaymentMethod"),PaymentProduct=lazyModel("PaymentProduct"),PaymentTransaction=lazyModel("PaymentTransaction"),Post=lazyModel("Post"),Categorizable=lazyModel("Categorizable"),PrintDevice=lazyModel("PrintDevice"),Product=lazyModel("Product"),ProductUnit=lazyModel("ProductUnit"),ProductVariant=lazyModel("ProductVariant"),QueryLog=lazyModel("QueryLog"),Receipt=lazyModel("Receipt"),Referral=lazyModel("Referral"),ReferralCode=lazyModel("ReferralCode"),Release=lazyModel("Release"),Request=lazyModel("Request"),Review=lazyModel("Review"),ShippingMethod=lazyModel("ShippingMethod"),ShippingRate=lazyModel("ShippingRate"),ShippingZone=lazyModel("ShippingZone"),SocialPost=lazyModel("SocialPost"),SocialAccount=lazyModel("SocialAccount"),Subscriber=lazyModel("Subscriber"),SubscriberEmail=lazyModel("SubscriberEmail"),Subscription=lazyModel("Subscription"),Tag=lazyModel("Tag"),TaxRate=lazyModel("TaxRate"),Team=lazyModel("Team"),TeamInvitation=lazyModel("TeamInvitation"),TeamMember=lazyModel("TeamMember"),Transaction=lazyModel("Transaction"),WaitlistProduct=lazyModel("WaitlistProduct"),WaitlistRestaurant=lazyModel("WaitlistRestaurant"),Websocket=lazyModel("Websocket");const _allExports={User,Job,FailedJob,Activity,AnalyticsEvent,Automation,AutomationRun,Auction,AuctionItem,Author,Bid,Board,BoardColumn,Campaign,CampaignSend,CampaignVariant,Card,CardComment,Cart,CartItem,Categorizable,Category,Comment,Coupon,Customer,CommunicationSuppression,ConsentEvent,DeliveryRoute,DeliveryStop,Deployment,DigitalDelivery,Courier,CourierPing,EmailList,EmailListSubscriber,EmailIdempotency,EmailSuppression,EmailWebhookEvent,ErrorModel,Form,FormField,FormSubmission,GdprRequest,GiftCard,Label,LicenseKey,Log,LoyaltyPoint,LoyaltyReward,MailPreference,Manufacturer,Notification,MagicLinkToken,Menu,MenuItem,NotificationDelivery,Order,OrderIdempotency,OrderItem,Page,PageRevision,Payment,PaymentMethod,PaymentProduct,PaymentTransaction,PaymentWebhookEvent,Pledge,Post,PrintDevice,Product,ProductUnit,ProductVariant,QueryLog,Receipt,Release,Request,Review,ShippingMethod,Redirect,SenderDomain,ShippingRate,ShippingZone,Site,SiteDomain,SmsOptOut,SocialAccount,SocialPost,Subscriber,SubscriberEmail,Subscription,Tag,TaxRate,Team,TeamInvitation,TeamMember,Transaction,UsageEvent,WaitlistProduct,WaitlistRestaurant,Websocket},g=globalThis;for(const[name,M]of Object.entries(_allExports))if(M&&g[name]===void 0)g[name]=M;export const modelGlobalNames=Object.keys(_allExports);async function _safeAll(M){if(!M)return[];try{if(typeof M.all==="function"){const rows=await M.all();return Array.isArray(rows)?rows:[]}if(typeof M.get==="function"){const rows=await M.get();return Array.isArray(rows)?rows:[]}}catch{}return[]}function _safeGet(row,key,fallback=""){if(!row)return fallback;try{if(typeof row.get==="function"){const v=row.get(key);if(v!==void 0&&v!==null)return v}}catch{}const direct=row?.[key];return direct!==void 0&&direct!==null?direct:fallback}if(g.safeAll===void 0)g.safeAll=_safeAll;if(g.safeGet===void 0)g.safeGet=_safeGet;export*from"./messaging";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { QueryBuilderOptions } from '@stacksjs/query-builder';
|
|
2
|
+
/**
|
|
3
|
+
* Split a project's `config/qb.ts` into the behaviour to apply and a report on
|
|
4
|
+
* the connection settings it tried to make.
|
|
5
|
+
*
|
|
6
|
+
* A connection key that matches what is already in force is dropped silently:
|
|
7
|
+
* a qb.ts that restates the connection is redundant, not wrong.
|
|
8
|
+
*/
|
|
9
|
+
export declare function splitProjectQueryBuilderConfig(project: QueryBuilderOptions | null | undefined, active: ActiveConnection): ProjectQueryBuilderConfig;
|
|
10
|
+
/** The warning logged when `config/qb.ts` tried to choose the database. */
|
|
11
|
+
export declare function connectionConflictWarning(conflicts: readonly string[]): string;
|
|
12
|
+
/** The keys that choose the database, rather than how queries behave. */
|
|
13
|
+
export declare const CONNECTION_KEYS: readonly ['dialect', 'database'];
|
|
14
|
+
export declare interface ProjectQueryBuilderConfig {
|
|
15
|
+
behaviour: QueryBuilderBehaviour
|
|
16
|
+
conflicts: string[]
|
|
17
|
+
}
|
|
18
|
+
/** The connection the query builder is using now, as `config` reports it. */
|
|
19
|
+
export declare interface ActiveConnection {
|
|
20
|
+
dialect?: string
|
|
21
|
+
database?: { database?: string }
|
|
22
|
+
}
|
|
23
|
+
declare type ConnectionKey = typeof CONNECTION_KEYS[number];
|
|
24
|
+
/** What `config/qb.ts` may set: everything except the connection. */
|
|
25
|
+
export type QueryBuilderBehaviour = Omit<QueryBuilderOptions, ConnectionKey>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CONNECTION_KEYS=["dialect","database"];export function splitProjectQueryBuilderConfig(project,active){if(!project||typeof project!=="object")return{behaviour:{},conflicts:[]};const{dialect,database,...behaviour}=project,conflicts=[];if(dialect!==void 0&&dialect!==active.dialect)conflicts.push(`dialect '${String(dialect)}' (config/database.ts resolved '${active.dialect??"nothing"}')`);const named=database?.database;if(named!==void 0&&named!==active.database?.database)conflicts.push(`database '${String(named)}' (config/database.ts resolved '${active.database?.database??"nothing"}')`);return{behaviour,conflicts}}export function connectionConflictWarning(conflicts){return`[orm] config/qb.ts sets the connection, which belongs to config/database.ts, so it was ignored: ${conflicts.join("; ")}. Remove \`dialect\` and \`database\` from config/qb.ts.`}
|
package/dist/routes.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{parseBearerToken,route}from"@stacksjs/router";import{env}from"@stacksjs/env";import{projectPath,storagePath}from"@stacksjs/path";import{createQueryBuilder,defaultConfig,setConfig}from"@stacksjs/query-builder";import{HttpError}from"@stacksjs/error-handling";import{log}from"@stacksjs/logging/runtime";import{API_ABILITIES,apiBasePath,applyCasts,applySorting,buildIndexPaginator,buildReadColumnMap,dashboardRoleMiddleware,describeContradictoryRoleGates,describeUnscopedMutatingModels,dropHiddenInputs,filterFillable,getWritableFields,indexRouteShapes,mapWriteError,ownershipDeclaredUnscoped,policyDecision,resolveAbilityMiddleware,resolveIndexPageArgs,resolveRowScopingPolicy,routeShapeKey,stampOwnership,stripHidden,toSnakeCase,toSnakeCaseKeys,validateWriteBody}from"./auto-crud";import{loadModelRegistryWithOrigins,resolveModelApiSelection,selectApiModels}from"./model-registry";import{effectiveOwnershipConfig}from"./ownership";const qbConfigPath=projectPath("config/qb.ts");try{const projectQbConfig=(await import(qbConfigPath)).default;setConfig(projectQbConfig??defaultConfig)}catch{log.debug("[orm] No config/qb.ts override found - deriving config from DB_CONNECTION");const connection=env.DB_CONNECTION||"sqlite",libsql=connection==="turso"||connection==="libsql",dialect=libsql?"sqlite":connection;setConfig({...defaultConfig,dialect,database:libsql?{database:env.TURSO_DATABASE_URL||"",url:env.TURSO_DATABASE_URL||"",authToken:env.TURSO_AUTH_TOKEN||void 0}:dialect==="sqlite"?{database:env.DB_DATABASE_PATH||"database/stacks.sqlite"}:{database:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT||(dialect==="postgres"?5432:3306),username:env.DB_USERNAME||(dialect==="postgres"?"postgres":"root"),password:env.DB_PASSWORD||""}})}const registry=await loadModelRegistryWithOrigins({defaultsRoot:storagePath("framework/defaults/app/Models"),userRoot:projectPath("app/Models"),onImportError:(file,error)=>log.warn(`[orm] Could not load model ${file}: ${error.message}`)}),models=registry.models;function dynamicDb(){return createQueryBuilder()}const routeShapeIndex=indexRouteShapes(route.routes);function routeClaimed(method,path){const key=routeShapeKey(method,path),existing=routeShapeIndex.get(key);if(!existing){routeShapeIndex.set(key,{method,path});return!1}const existingPath=String(existing.path??"");log.debug(`[orm] Skipping generated ${method} ${path} - already defined${existingPath&&existingPath!==path?` as ${existingPath}`:""}.`);return!0}function getHiddenFields(model){if(!model.attributes)return[];return Object.entries(model.attributes).filter(([_,attr])=>attr.hidden===!0).map(([name])=>name)}function pluralize(s){if(s.endsWith("y")&&!/[aeiou]y$/i.test(s))return`${s.slice(0,-1)}ies`;if(/(?:s|x|z|ch|sh)$/i.test(s))return`${s}es`;return`${s}s`}async function applyIncludes(rows,model,includeParam,modelsRegistry){if(!rows.length)return rows;const requested=includeParam.split(",").map((s)=>s.trim()).filter(Boolean);if(!requested.length)return rows;const belongsTo=Array.isArray(model.belongsTo)?model.belongsTo:[],hasMany=Array.isArray(model.hasMany)?model.hasMany:[],hasOne=Array.isArray(model.hasOne)?model.hasOne:[],allowed=new Map;for(const m of belongsTo)allowed.set(toSnakeCase(m),{kind:"belongsTo",modelName:m});for(const m of hasOne)allowed.set(toSnakeCase(m),{kind:"hasOne",modelName:m});for(const m of hasMany)allowed.set(pluralize(toSnakeCase(m)),{kind:"hasMany",modelName:m});for(const include of requested){const meta=allowed.get(include);if(!meta)continue;const related=modelsRegistry[meta.modelName];if(!related)continue;const relatedTable=related.table||pluralize(toSnakeCase(meta.modelName)),relatedHidden=getHiddenFields(related),relatedAttrs=(id)=>stripHidden(applyReadCasts(id,related),relatedHidden);if(meta.kind==="belongsTo"||meta.kind==="hasOne"){const fkOnParent=`${toSnakeCase(meta.modelName)}_id`,ids=[...new Set(rows.map((r)=>r[fkOnParent]).filter((v)=>v!=null))];if(!ids.length){for(const r of rows)r[include]=null;continue}const childRows=await dynamicDb().selectFrom(relatedTable).whereIn("id",ids).get(),byId=new Map;for(const cr of childRows??[])byId.set(String(cr.id),relatedAttrs(cr));for(const r of rows)r[include]=byId.get(String(r[fkOnParent]))??null}else{const parentName=String(model.name??""),fkOnChild=`${toSnakeCase(parentName)}_id`,parentIds=[...new Set(rows.map((r)=>r.id).filter((v)=>v!=null))];if(!parentIds.length){for(const r of rows)r[include]=[];continue}const childRows=await dynamicDb().selectFrom(relatedTable).whereIn(fkOnChild,parentIds).get(),grouped=new Map;for(const cr of childRows??[]){const key=String(cr[fkOnChild]),arr=grouped.get(key)??[];arr.push(relatedAttrs(cr));grouped.set(key,arr)}for(const r of rows)r[include]=grouped.get(String(r.id))??[]}}return rows}async function applyDefinedSetters(data,model){const setters=model?.set;if(!setters||typeof setters!=="object")return data;const out={...data};for(const[key,fn]of Object.entries(setters)){if(typeof fn!=="function"||!(key in out))continue;try{out[key]=await fn(out)}catch{}}return out}function applySetCasts(data,model){return applyCasts(data,model?.casts,"set")}function applyReadCasts(row,model){return applyCasts(row,model?.casts,"get")}function jsonResponse(data,status=200){return new Response(JSON.stringify(data),{status,headers:{"Content-Type":"application/json"}})}function errorResponse(req,body,status=400){const reqId=req?._requestId,enriched=reqId?{...body,request_id:reqId}:body;return new Response(JSON.stringify(enriched),{status,headers:{"Content-Type":"application/json"}})}const MAX_BODY_BYTES=1048576;async function getRequestBody(req){if(Number(req.headers?.get?.("content-length")??0)>MAX_BODY_BYTES)throw new HttpError(413,`Request body exceeds ${MAX_BODY_BYTES}-byte limit`);if(req.jsonBody&&typeof req.jsonBody==="object")return req.jsonBody;if(req.formBody&&typeof req.formBody==="object")return req.formBody;try{const text=await req.clone().text();if(text.length>MAX_BODY_BYTES)throw new HttpError(413,`Request body exceeds ${MAX_BODY_BYTES}-byte limit`);if(!text)return{};const parsed=JSON.parse(text);return parsed&&typeof parsed==="object"&&!Array.isArray(parsed)?parsed:{}}catch(err){if(err instanceof HttpError)throw err;throw new HttpError(400,`Invalid JSON body: ${err.message}`)}}function coerceId(raw){if(raw==null)return null;if(typeof raw==="number")return Number.isFinite(raw)&&raw>0?raw:null;if(typeof raw==="string"){const trimmed=raw.trim();if(!trimmed)return null;if(/^\d+$/.test(trimmed)){const n=Number(trimmed);return Number.isFinite(n)&&n>0?n:null}return trimmed}return null}function bearerOf(req){const fn=req.bearerToken;if(typeof fn==="function"){const t=fn.call(req);if(t)return t}return parseBearerToken(req.headers?.get?.("authorization")??null)}async function authedUserFromRequest(req){const stored=req._authenticatedUser;if(stored)return stored;const token=bearerOf(req);if(!token)return null;try{const{Auth}=await import("@stacksjs/auth"),user=await Auth.getUserFromToken(token);if(user)req._authenticatedUser=user;return user||null}catch{return null}}let authGateModule;async function authGate(){try{const{Gate}=authGateModule??=await import("@stacksjs/auth");return Gate}catch{return null}}async function hasPolicyFor(modelName){const gate=await authGate();return!!gate&&gate.hasPolicy(modelName)}async function policyRefusal(modelName,ability,req,subject){const refusal=await policyDecision(modelName,ability,{gate:authGate,user:()=>authedUserFromRequest(req)},subject);return refusal?jsonResponse({error:refusal.message},refusal.status):null}function ownsRow(rowField,ownerValue){if(rowField==null||ownerValue==null)return!1;if(Array.isArray(ownerValue))return ownerValue.some((v)=>String(v)===String(rowField));return String(rowField)===String(ownerValue)}async function resolveOwnership(model,user,req){const cfg=effectiveOwnershipConfig(model);if(!cfg||!cfg.field||typeof cfg.resolve!=="function")return{enforced:!1,value:null,field:"",bypass:!1};const bypass=typeof cfg.bypass==="function"?!!cfg.bypass(user,req):!1;let value=null;try{value=await cfg.resolve(user,req)}catch{value=null}return{enforced:!0,value,field:String(cfg.field),bypass}}async function resolveAuthedFill(model,hook,user,req){const cfg=model?.authedFill?.[hook];if(!cfg)return{};if(typeof cfg==="function")try{const result=await cfg(user,req);return result&&typeof result==="object"?result:{}}catch{return{}}if(typeof cfg==="object"){const out={};for(const[field,getter]of Object.entries(cfg))try{out[field]=typeof getter==="function"?await getter(user,req):getter}catch{}return out}return{}}const securityApiConfig=await(async()=>{try{return(await import(projectPath("config/security.ts"))).default?.api}catch{return}})(),rowScopingPolicy=resolveRowScopingPolicy(securityApiConfig?.rowScoping,process.env.STACKS_API_ROW_SCOPING),apiModels=(()=>{const selection=resolveModelApiSelection(securityApiConfig?.models,process.env.STACKS_MODEL_APIS),{models:selected,unmatched}=selectApiModels(registry,selection);if(unmatched.length>0){const setting=selection.source==="env"?"STACKS_MODEL_APIS":"security.api.models";log.warn(`[orm] ${setting} names ${unmatched.length===1?"a model":"models"} with no \`useApi\` trait, so no API was generated for ${unmatched.join(", ")}.`)}return selected})(),unscopedMutatingModels=[],contradictoryRoleGates=[];for(const[modelName,model]of Object.entries(apiModels)){const useApi=model.traits?.useApi;if(!useApi)continue;const apiConfig=typeof useApi==="object"?useApi:{},uri=apiConfig.uri||model.table||modelName.toLowerCase()+"s",enabledRoutes=apiConfig.routes||["index","show","store","update","destroy"],table=model.table||uri,fillableFields=getWritableFields(model),hiddenFields=getHiddenFields(model),basePath=apiBasePath(uri,apiConfig.prefix),readColumns=buildReadColumnMap(model.attributes,hiddenFields),{abilities,declared}=resolveAbilityMiddleware(useApi,model.dashboard);if(dashboardRoleMiddleware(model.dashboard)){const open=API_ABILITIES.filter((ability)=>enabledRoutes.includes(ability)&&abilities[ability].length===0);if(open.length>0)contradictoryRoleGates.push({model:modelName,abilities:open})}const hasMutating=["store","update","destroy"].some((r)=>enabledRoutes.includes(r)),openWrites=API_ABILITIES.filter((ability)=>ability!=="index"&&ability!=="show"&&enabledRoutes.includes(ability)&&abilities[ability].length===0);if(declared&&openWrites.length>0)log.warn(`[orm] ${modelName}: registering UNAUTHENTICATED ${openWrites.join(", ")} at ${basePath} (explicit empty \`middleware\` opt-out)`);const openReads=API_ABILITIES.filter((ability)=>(ability==="index"||ability==="show")&&enabledRoutes.includes(ability)&&abilities[ability].length===0);if(declared&&openReads.length>0)log.debug(`[orm] ${modelName}: registering public ${openReads.join(", ")} at ${basePath} (declared an empty read \`middleware\`)`);const rowScoped=!!effectiveOwnershipConfig(model),forceAuth=(names)=>rowScoped&&!names.includes("auth")?["auth",...names]:names,indexMiddleware=forceAuth(abilities.index),showMiddleware=forceAuth(abilities.show),declaredUnscoped=ownershipDeclaredUnscoped(model),writesAreUnscoped=hasMutating&&!rowScoped&&!declaredUnscoped;if(writesAreUnscoped)unscopedMutatingModels.push(modelName);if(hasMutating&&declaredUnscoped)log.debug(`[orm] ${modelName}: registering unscoped mutating routes at ${basePath} (declared \`ownership: false\`)`);const skipMutatingRoutes=writesAreUnscoped&&rowScopingPolicy==="deny",applyMiddleware=(chain,names)=>{let r=chain;for(const name of names)if(r&&typeof r.middleware==="function")r=r.middleware(name);return r};if(enabledRoutes.includes("index")&&!routeClaimed("GET",basePath))applyMiddleware(route.get(basePath,async(req)=>{try{const url=new URL(req.url),{page,perPage,offset}=resolveIndexPageArgs(url.searchParams),sort=url.searchParams.get("sort"),refusedList=await policyRefusal(modelName,"viewAny",req);if(refusedList)return refusedList;let query=dynamicDb().selectFrom(table);query=applySorting(query,sort,readColumns);const RESERVED=new Set(["page","per_page","sort","fields","search","include","limit","offset","with_count","withTrashed","onlyTrashed"]);for(const[key,value]of url.searchParams.entries()){if(RESERVED.has(key)||!value)continue;if(!/^[a-z_][a-z0-9_]*$/i.test(key))continue;const col=readColumns.get(key);if(col)query=query.where(col,"=",value)}const searchTerm=url.searchParams.get("search");if(searchTerm&&fillableFields.length>0){const textFields=fillableFields.slice(0,5);query=query.where((qb)=>{for(const field of textFields)qb=qb.orWhere(toSnakeCase(field),"like",`%${searchTerm}%`);return qb})}const fieldsParam=url.searchParams.get("fields");if(fieldsParam){const selectedFields=fieldsParam.split(",").map((f)=>f.trim()).filter((f)=>/^[a-z_][a-z0-9_]*$/i.test(f)).map((f)=>toSnakeCase(f));if(selectedFields.length>0)query=query.select(selectedFields)}const usesSoftDeletes=!!model.traits?.useSoftDeletes,withTrashed=url.searchParams.get("withTrashed")==="true",onlyTrashed=url.searchParams.get("onlyTrashed")==="true";if(usesSoftDeletes&&!withTrashed)query=onlyTrashed?query.whereNotNull("deleted_at"):query.whereNull("deleted_at");const own=rowScoped?await resolveOwnership(model,await authedUserFromRequest(req),req):{enforced:!1,value:null,field:"",bypass:!1};if(own.enforced&&!own.bypass){if(own.value==null||Array.isArray(own.value)&&own.value.length===0){const emptyPaginator=buildIndexPaginator(url,page,perPage,0,!1,void 0);return new Response(JSON.stringify({data:[],...emptyPaginator}),{status:200,headers:{"Content-Type":"application/json","Cache-Control":"private, no-store",Vary:"Authorization"}})}query=Array.isArray(own.value)?query.where(own.field,"in",own.value):query.where(own.field,"=",own.value)}const rawResults=await query.limit(perPage+1).offset(offset).get()||[],hasMore=rawResults.length>perPage;let records=(hasMore?rawResults.slice(0,perPage):rawResults).map((r)=>stripHidden(applyReadCasts(r,model),hiddenFields));const includeParam=url.searchParams.get("include");if(includeParam)records=await applyIncludes(records,model,includeParam,models);const wantCount=url.searchParams.get("with_count")==="true";let total;if(wantCount)try{let countQuery=dynamicDb().selectFrom(table);if(own.enforced&&!own.bypass&&own.value!=null)countQuery=Array.isArray(own.value)?countQuery.where(own.field,"in",own.value):countQuery.where(own.field,"=",own.value);const raw=await countQuery.count(),n=typeof raw==="number"?raw:Number(raw?.count??raw);if(Number.isFinite(n))total=n}catch{}const respHeaders={"Content-Type":"application/json"};if(total!==void 0&&!Number.isNaN(total))respHeaders["X-Total-Count"]=String(total);respHeaders["Cache-Control"]=own.enforced&&!own.bypass?"private, no-store":"public, max-age=15, must-revalidate";respHeaders.Vary="Authorization";const paginator=buildIndexPaginator(url,page,perPage,records.length,hasMore,total);return new Response(JSON.stringify({data:records,...paginator}),{status:200,headers:respHeaders})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to fetch ${uri}`,detail:String(err)},500)}}),indexMiddleware);if(enabledRoutes.includes("show")&&!routeClaimed("GET",`${basePath}/{id}`))applyMiddleware(route.get(`${basePath}/{id}`,async(req)=>{try{const id=coerceId(req.params?.id);if(id==null)return jsonResponse({error:"Invalid ID parameter"},400);const result=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!result)return jsonResponse({error:`${modelName} not found`},404);const url=new URL(req.url);if(model.traits?.useSoftDeletes&&result.deleted_at&&url.searchParams.get("withTrashed")!=="true")return jsonResponse({error:`${modelName} not found`},404);const own=rowScoped?await resolveOwnership(model,await authedUserFromRequest(req),req):{enforced:!1,value:null,field:"",bypass:!1};if(own.enforced&&!own.bypass){if(own.value==null||!ownsRow(result[own.field],own.value))return jsonResponse({error:`${modelName} not found`},404)}const refusedView=await policyRefusal(modelName,"view",req,result);if(refusedView)return refusedView;let payload=stripHidden(applyReadCasts(result,model),hiddenFields);const includeParam=url.searchParams.get("include");if(includeParam){const[withRel]=await applyIncludes([payload],model,includeParam,models);payload=withRel}const lastWrite=payload?.updated_at||payload?.created_at,etag=lastWrite?`W/"${payload.id}-${String(lastWrite)}"`:void 0,ifNoneMatch=req.headers?.get?.("if-none-match");if(etag&&ifNoneMatch&&ifNoneMatch===etag)return new Response(null,{status:304,headers:{ETag:etag}});const headers={"Content-Type":"application/json"};if(etag)headers.ETag=etag;headers["Cache-Control"]=own.enforced&&!own.bypass?"private, no-store":"public, max-age=30, must-revalidate";headers.Vary="Authorization";return new Response(JSON.stringify({data:payload}),{status:200,headers})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to fetch ${modelName}`,detail:String(err)},500)}}),showMiddleware);if(!skipMutatingRoutes&&enabledRoutes.includes("store")&&!routeClaimed("POST",basePath))applyMiddleware(route.post(basePath,async(req)=>{try{const body=await getRequestBody(req),safeBody=dropHiddenInputs(body,hiddenFields);let data=filterFillable(safeBody,fillableFields);const authedUser=await authedUserFromRequest(req);if(authedUser){const stamped=await resolveAuthedFill(model,"creating",authedUser,req);for(const[k,v]of Object.entries(stamped))if(v!==void 0&&v!==null&&data[k]===void 0)data[k]=v}const refusedCreate=await policyRefusal(modelName,"create",req);if(refusedCreate)return refusedCreate;const own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);const stamped=stampOwnership(data,own.field,own.value);if(stamped.error)return jsonResponse({error:stamped.error},403);data=stamped.data}if(Object.keys(data).length===0)return jsonResponse({error:"No fillable fields provided"},422);const v=validateWriteBody(data,model,"creating");if(!v.valid)return jsonResponse({success:!1,message:"Validation failed",errors:v.errors},422);if(model.traits?.useTimestamps!==!1){const now=new Date().toISOString();data.created_at=now;data.updated_at=now}const hookedData=await applyDefinedSetters(data,model),writeData=toSnakeCaseKeys(applySetCasts(hookedData,model)),result=await dynamicDb().insertInto(table).values(writeData).execute();let created=writeData;try{const lastId=result?.lastInsertRowid??result?.insertId??result;if(lastId){const fetched=await dynamicDb().selectFrom(table).where({id:lastId}).executeTakeFirst();if(fetched)created=fetched}}catch{}return jsonResponse({data:stripHidden(applyReadCasts(created,model),hiddenFields)},201)}catch(err){const{status,body}=mapWriteError(err,modelName,"create");return jsonResponse(body,status)}}),abilities.store);if(!skipMutatingRoutes&&enabledRoutes.includes("update")){const updateHandler=async(req)=>{try{const id=coerceId(req.params?.id);if(id==null)return jsonResponse({error:"Invalid ID parameter"},400);const body=await getRequestBody(req),safeBody=dropHiddenInputs(body,hiddenFields),data=filterFillable(safeBody,fillableFields),authedUser=await authedUserFromRequest(req);if(authedUser){const stamped=await resolveAuthedFill(model,"updating",authedUser,req);for(const[k,v]of Object.entries(stamped))if(v!==void 0&&v!==null&&data[k]===void 0)data[k]=v}if(Object.keys(data).length===0)return jsonResponse({error:"No fillable fields provided"},422);const v=validateWriteBody(data,model,"updating");if(!v.valid)return jsonResponse({success:!1,message:"Validation failed",errors:v.errors},422);const existing=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!existing)return jsonResponse({error:`${modelName} not found`},404);const refusedUpdate=await policyRefusal(modelName,"update",req,existing);if(refusedUpdate)return refusedUpdate;const own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);const rowOwner=existing[own.field];if(!ownsRow(rowOwner,own.value))return jsonResponse({error:`Not your ${modelName}`},403);const submittedOwnerKey=Object.keys(data).find((k)=>toSnakeCase(k)===toSnakeCase(own.field));if(submittedOwnerKey!==void 0&&!ownsRow(data[submittedOwnerKey],own.value))return jsonResponse({error:`Cannot reassign ${modelName} ownership`},403)}if(model.traits?.useTimestamps!==!1)data.updated_at=new Date().toISOString();const hookedData=await applyDefinedSetters(data,model),writeData=toSnakeCaseKeys(applySetCasts(hookedData,model));await dynamicDb().updateTable(table).set(writeData).where({id}).execute();let updated={...existing,...writeData};try{const fetched=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(fetched)updated=fetched}catch{}return jsonResponse({data:stripHidden(applyReadCasts(updated,model),hiddenFields)})}catch(err){const{status,body}=mapWriteError(err,modelName,"update");return jsonResponse(body,status)}};if(!routeClaimed("PUT",`${basePath}/{id}`))applyMiddleware(route.put(`${basePath}/{id}`,updateHandler),abilities.update);if(!routeClaimed("PATCH",`${basePath}/{id}`))applyMiddleware(route.patch(`${basePath}/{id}`,updateHandler),abilities.update)}const usesSoftDeletes=!!model.traits?.useSoftDeletes;if(!skipMutatingRoutes&&enabledRoutes.includes("destroy")&&!routeClaimed("DELETE",`${basePath}/{id}`))applyMiddleware(route.delete(`${basePath}/{id}`,async(req)=>{try{const id=coerceId(req.params?.id);if(id==null)return jsonResponse({error:"Invalid ID parameter"},400);const existing=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!existing)return jsonResponse({error:`${modelName} not found`},404);const authedUser=await authedUserFromRequest(req),own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);const rowOwner=existing[own.field];if(!ownsRow(rowOwner,own.value))return jsonResponse({error:`Not your ${modelName}`},403)}const refusedDelete=await policyRefusal(modelName,"delete",req,existing);if(refusedDelete)return refusedDelete;if(usesSoftDeletes){const now=new Date().toISOString();await dynamicDb().updateTable(table).set({deleted_at:now,updated_at:now}).where({id}).execute()}else await dynamicDb().deleteFrom(table).where({id}).execute();return new Response(null,{status:204})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to delete ${modelName}`,detail:String(err)},500)}}),abilities.destroy);if(!skipMutatingRoutes&&enabledRoutes.includes("destroy")&&!routeClaimed("POST",`${basePath}/bulk-delete`))applyMiddleware(route.post(`${basePath}/bulk-delete`,async(req)=>{try{const ids=(await getRequestBody(req))?.ids;if(!Array.isArray(ids)||ids.length===0)return jsonResponse({error:"An array of IDs is required"},422);if(ids.length>100)return jsonResponse({error:"Cannot delete more than 100 records at once"},422);const validIds=[],invalidIds=[];for(const raw of ids){const coerced=coerceId(raw);if(coerced!=null)validIds.push(coerced);else invalidIds.push(raw)}if(invalidIds.length>0)return jsonResponse({error:"Invalid IDs in batch",invalid:invalidIds},422);const authedUser=await authedUserFromRequest(req),own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);if(own.value==null)return jsonResponse({error:"Caller has no ownership identity"},403);const rows=await dynamicDb().selectFrom(table).select(["id",own.field]).execute(),ownedIds=new Set(rows.filter((r)=>ownsRow(r[own.field],own.value)).map((r)=>String(r.id))),notOwned=validIds.filter((id)=>!ownedIds.has(String(id)));if(notOwned.length>0)return jsonResponse({error:`Cannot delete ${modelName} you don't own`,ids:notOwned},403)}if(await hasPolicyFor(modelName)){const refusedRows=[];for(const id of validIds){const row=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!row)continue;if(await policyRefusal(modelName,"delete",req,row))refusedRows.push(id)}if(refusedRows.length>0)return jsonResponse({error:`Cannot delete ${modelName} this policy refuses`,ids:refusedRows},403)}const now=new Date().toISOString();for(const id of validIds)if(usesSoftDeletes)await dynamicDb().updateTable(table).set({deleted_at:now,updated_at:now}).where({id}).execute();else await dynamicDb().deleteFrom(table).where({id}).execute();return jsonResponse({message:`Successfully deleted ${validIds.length} ${uri}`})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to bulk delete ${uri}`,detail:String(err)},500)}}),abilities.destroy)}{const report=describeUnscopedMutatingModels(unscopedMutatingModels,rowScopingPolicy);if(report)log.warn(report);const contradictions=describeContradictoryRoleGates(contradictoryRoleGates);if(contradictions)log.warn(contradictions)}export default route;
|
|
1
|
+
import process from"node:process";import{parseBearerToken,route}from"@stacksjs/router";import{projectPath,storagePath}from"@stacksjs/path";import{createQueryBuilder,config as queryBuilderConfig,setConfig}from"@stacksjs/query-builder";import{HttpError}from"@stacksjs/error-handling";import{log}from"@stacksjs/logging/runtime";import{API_ABILITIES,apiBasePath,applyCasts,applySorting,buildIndexPaginator,buildReadColumnMap,dashboardRoleMiddleware,describeContradictoryRoleGates,describeUnscopedMutatingModels,dropHiddenInputs,filterFillable,getWritableFields,indexRouteShapes,mapWriteError,ownershipDeclaredUnscoped,policyDecision,resolveAbilityMiddleware,resolveIndexPageArgs,resolveRowScopingPolicy,routeShapeKey,stampOwnership,stripHidden,toSnakeCase,toSnakeCaseKeys,validateWriteBody}from"./auto-crud";import{loadModelRegistryWithOrigins,resolveModelApiSelection,selectApiModels}from"./model-registry";import{effectiveOwnershipConfig}from"./ownership";import{connectionConflictWarning,splitProjectQueryBuilderConfig}from"./project-query-builder-config";const qbConfigPath=projectPath("config/qb.ts");let projectQbConfig;try{projectQbConfig=(await import(qbConfigPath)).default}catch{log.debug("[orm] No config/qb.ts override found - using the connection from config/database.ts")}if(projectQbConfig){const{behaviour,conflicts}=splitProjectQueryBuilderConfig(projectQbConfig,queryBuilderConfig);if(conflicts.length)log.warn(connectionConflictWarning(conflicts));setConfig(behaviour)}const registry=await loadModelRegistryWithOrigins({defaultsRoot:storagePath("framework/defaults/app/Models"),userRoot:projectPath("app/Models"),onImportError:(file,error)=>log.warn(`[orm] Could not load model ${file}: ${error.message}`)}),models=registry.models;function dynamicDb(){return createQueryBuilder()}const routeShapeIndex=indexRouteShapes(route.routes);function routeClaimed(method,path){const key=routeShapeKey(method,path),existing=routeShapeIndex.get(key);if(!existing){routeShapeIndex.set(key,{method,path});return!1}const existingPath=String(existing.path??"");log.debug(`[orm] Skipping generated ${method} ${path} - already defined${existingPath&&existingPath!==path?` as ${existingPath}`:""}.`);return!0}function getHiddenFields(model){if(!model.attributes)return[];return Object.entries(model.attributes).filter(([_,attr])=>attr.hidden===!0).map(([name])=>name)}function pluralize(s){if(s.endsWith("y")&&!/[aeiou]y$/i.test(s))return`${s.slice(0,-1)}ies`;if(/(?:s|x|z|ch|sh)$/i.test(s))return`${s}es`;return`${s}s`}async function applyIncludes(rows,model,includeParam,modelsRegistry){if(!rows.length)return rows;const requested=includeParam.split(",").map((s)=>s.trim()).filter(Boolean);if(!requested.length)return rows;const belongsTo=Array.isArray(model.belongsTo)?model.belongsTo:[],hasMany=Array.isArray(model.hasMany)?model.hasMany:[],hasOne=Array.isArray(model.hasOne)?model.hasOne:[],allowed=new Map;for(const m of belongsTo)allowed.set(toSnakeCase(m),{kind:"belongsTo",modelName:m});for(const m of hasOne)allowed.set(toSnakeCase(m),{kind:"hasOne",modelName:m});for(const m of hasMany)allowed.set(pluralize(toSnakeCase(m)),{kind:"hasMany",modelName:m});for(const include of requested){const meta=allowed.get(include);if(!meta)continue;const related=modelsRegistry[meta.modelName];if(!related)continue;const relatedTable=related.table||pluralize(toSnakeCase(meta.modelName)),relatedHidden=getHiddenFields(related),relatedAttrs=(id)=>stripHidden(applyReadCasts(id,related),relatedHidden);if(meta.kind==="belongsTo"||meta.kind==="hasOne"){const fkOnParent=`${toSnakeCase(meta.modelName)}_id`,ids=[...new Set(rows.map((r)=>r[fkOnParent]).filter((v)=>v!=null))];if(!ids.length){for(const r of rows)r[include]=null;continue}const childRows=await dynamicDb().selectFrom(relatedTable).whereIn("id",ids).get(),byId=new Map;for(const cr of childRows??[])byId.set(String(cr.id),relatedAttrs(cr));for(const r of rows)r[include]=byId.get(String(r[fkOnParent]))??null}else{const parentName=String(model.name??""),fkOnChild=`${toSnakeCase(parentName)}_id`,parentIds=[...new Set(rows.map((r)=>r.id).filter((v)=>v!=null))];if(!parentIds.length){for(const r of rows)r[include]=[];continue}const childRows=await dynamicDb().selectFrom(relatedTable).whereIn(fkOnChild,parentIds).get(),grouped=new Map;for(const cr of childRows??[]){const key=String(cr[fkOnChild]),arr=grouped.get(key)??[];arr.push(relatedAttrs(cr));grouped.set(key,arr)}for(const r of rows)r[include]=grouped.get(String(r.id))??[]}}return rows}async function applyDefinedSetters(data,model){const setters=model?.set;if(!setters||typeof setters!=="object")return data;const out={...data};for(const[key,fn]of Object.entries(setters)){if(typeof fn!=="function"||!(key in out))continue;try{out[key]=await fn(out)}catch{}}return out}function applySetCasts(data,model){return applyCasts(data,model?.casts,"set")}function applyReadCasts(row,model){return applyCasts(row,model?.casts,"get")}function jsonResponse(data,status=200){return new Response(JSON.stringify(data),{status,headers:{"Content-Type":"application/json"}})}function errorResponse(req,body,status=400){const reqId=req?._requestId,enriched=reqId?{...body,request_id:reqId}:body;return new Response(JSON.stringify(enriched),{status,headers:{"Content-Type":"application/json"}})}const MAX_BODY_BYTES=1048576;async function getRequestBody(req){if(Number(req.headers?.get?.("content-length")??0)>MAX_BODY_BYTES)throw new HttpError(413,`Request body exceeds ${MAX_BODY_BYTES}-byte limit`);if(req.jsonBody&&typeof req.jsonBody==="object")return req.jsonBody;if(req.formBody&&typeof req.formBody==="object")return req.formBody;try{const text=await req.clone().text();if(text.length>MAX_BODY_BYTES)throw new HttpError(413,`Request body exceeds ${MAX_BODY_BYTES}-byte limit`);if(!text)return{};const parsed=JSON.parse(text);return parsed&&typeof parsed==="object"&&!Array.isArray(parsed)?parsed:{}}catch(err){if(err instanceof HttpError)throw err;throw new HttpError(400,`Invalid JSON body: ${err.message}`)}}function coerceId(raw){if(raw==null)return null;if(typeof raw==="number")return Number.isFinite(raw)&&raw>0?raw:null;if(typeof raw==="string"){const trimmed=raw.trim();if(!trimmed)return null;if(/^\d+$/.test(trimmed)){const n=Number(trimmed);return Number.isFinite(n)&&n>0?n:null}return trimmed}return null}function bearerOf(req){const fn=req.bearerToken;if(typeof fn==="function"){const t=fn.call(req);if(t)return t}return parseBearerToken(req.headers?.get?.("authorization")??null)}async function authedUserFromRequest(req){const stored=req._authenticatedUser;if(stored)return stored;const token=bearerOf(req);if(!token)return null;try{const{Auth}=await import("@stacksjs/auth"),user=await Auth.getUserFromToken(token);if(user)req._authenticatedUser=user;return user||null}catch{return null}}let authGateModule;async function authGate(){try{const{Gate}=authGateModule??=await import("@stacksjs/auth");return Gate}catch{return null}}async function hasPolicyFor(modelName){const gate=await authGate();return!!gate&&gate.hasPolicy(modelName)}async function policyRefusal(modelName,ability,req,subject){const refusal=await policyDecision(modelName,ability,{gate:authGate,user:()=>authedUserFromRequest(req)},subject);return refusal?jsonResponse({error:refusal.message},refusal.status):null}function ownsRow(rowField,ownerValue){if(rowField==null||ownerValue==null)return!1;if(Array.isArray(ownerValue))return ownerValue.some((v)=>String(v)===String(rowField));return String(rowField)===String(ownerValue)}async function resolveOwnership(model,user,req){const cfg=effectiveOwnershipConfig(model);if(!cfg||!cfg.field||typeof cfg.resolve!=="function")return{enforced:!1,value:null,field:"",bypass:!1};const bypass=typeof cfg.bypass==="function"?!!cfg.bypass(user,req):!1;let value=null;try{value=await cfg.resolve(user,req)}catch{value=null}return{enforced:!0,value,field:String(cfg.field),bypass}}async function resolveAuthedFill(model,hook,user,req){const cfg=model?.authedFill?.[hook];if(!cfg)return{};if(typeof cfg==="function")try{const result=await cfg(user,req);return result&&typeof result==="object"?result:{}}catch{return{}}if(typeof cfg==="object"){const out={};for(const[field,getter]of Object.entries(cfg))try{out[field]=typeof getter==="function"?await getter(user,req):getter}catch{}return out}return{}}const securityApiConfig=await(async()=>{try{return(await import(projectPath("config/security.ts"))).default?.api}catch{return}})(),rowScopingPolicy=resolveRowScopingPolicy(securityApiConfig?.rowScoping,process.env.STACKS_API_ROW_SCOPING),apiModels=(()=>{const selection=resolveModelApiSelection(securityApiConfig?.models,process.env.STACKS_MODEL_APIS),{models:selected,unmatched}=selectApiModels(registry,selection);if(unmatched.length>0){const setting=selection.source==="env"?"STACKS_MODEL_APIS":"security.api.models";log.warn(`[orm] ${setting} names ${unmatched.length===1?"a model":"models"} with no \`useApi\` trait, so no API was generated for ${unmatched.join(", ")}.`)}return selected})(),unscopedMutatingModels=[],contradictoryRoleGates=[];for(const[modelName,model]of Object.entries(apiModels)){const useApi=model.traits?.useApi;if(!useApi)continue;const apiConfig=typeof useApi==="object"?useApi:{},uri=apiConfig.uri||model.table||modelName.toLowerCase()+"s",enabledRoutes=apiConfig.routes||["index","show","store","update","destroy"],table=model.table||uri,fillableFields=getWritableFields(model),hiddenFields=getHiddenFields(model),basePath=apiBasePath(uri,apiConfig.prefix),readColumns=buildReadColumnMap(model.attributes,hiddenFields),{abilities,declared}=resolveAbilityMiddleware(useApi,model.dashboard);if(dashboardRoleMiddleware(model.dashboard)){const open=API_ABILITIES.filter((ability)=>enabledRoutes.includes(ability)&&abilities[ability].length===0);if(open.length>0)contradictoryRoleGates.push({model:modelName,abilities:open})}const hasMutating=["store","update","destroy"].some((r)=>enabledRoutes.includes(r)),openWrites=API_ABILITIES.filter((ability)=>ability!=="index"&&ability!=="show"&&enabledRoutes.includes(ability)&&abilities[ability].length===0);if(declared&&openWrites.length>0)log.warn(`[orm] ${modelName}: registering UNAUTHENTICATED ${openWrites.join(", ")} at ${basePath} (explicit empty \`middleware\` opt-out)`);const openReads=API_ABILITIES.filter((ability)=>(ability==="index"||ability==="show")&&enabledRoutes.includes(ability)&&abilities[ability].length===0);if(declared&&openReads.length>0)log.debug(`[orm] ${modelName}: registering public ${openReads.join(", ")} at ${basePath} (declared an empty read \`middleware\`)`);const rowScoped=!!effectiveOwnershipConfig(model),forceAuth=(names)=>rowScoped&&!names.includes("auth")?["auth",...names]:names,indexMiddleware=forceAuth(abilities.index),showMiddleware=forceAuth(abilities.show),declaredUnscoped=ownershipDeclaredUnscoped(model),writesAreUnscoped=hasMutating&&!rowScoped&&!declaredUnscoped;if(writesAreUnscoped)unscopedMutatingModels.push(modelName);if(hasMutating&&declaredUnscoped)log.debug(`[orm] ${modelName}: registering unscoped mutating routes at ${basePath} (declared \`ownership: false\`)`);const skipMutatingRoutes=writesAreUnscoped&&rowScopingPolicy==="deny",applyMiddleware=(chain,names)=>{let r=chain;for(const name of names)if(r&&typeof r.middleware==="function")r=r.middleware(name);return r};if(enabledRoutes.includes("index")&&!routeClaimed("GET",basePath))applyMiddleware(route.get(basePath,async(req)=>{try{const url=new URL(req.url),{page,perPage,offset}=resolveIndexPageArgs(url.searchParams),sort=url.searchParams.get("sort"),refusedList=await policyRefusal(modelName,"viewAny",req);if(refusedList)return refusedList;let query=dynamicDb().selectFrom(table);query=applySorting(query,sort,readColumns);const RESERVED=new Set(["page","per_page","sort","fields","search","include","limit","offset","with_count","withTrashed","onlyTrashed"]);for(const[key,value]of url.searchParams.entries()){if(RESERVED.has(key)||!value)continue;if(!/^[a-z_][a-z0-9_]*$/i.test(key))continue;const col=readColumns.get(key);if(col)query=query.where(col,"=",value)}const searchTerm=url.searchParams.get("search");if(searchTerm&&fillableFields.length>0){const textFields=fillableFields.slice(0,5);query=query.where((qb)=>{for(const field of textFields)qb=qb.orWhere(toSnakeCase(field),"like",`%${searchTerm}%`);return qb})}const fieldsParam=url.searchParams.get("fields");if(fieldsParam){const selectedFields=fieldsParam.split(",").map((f)=>f.trim()).filter((f)=>/^[a-z_][a-z0-9_]*$/i.test(f)).map((f)=>toSnakeCase(f));if(selectedFields.length>0)query=query.select(selectedFields)}const usesSoftDeletes=!!model.traits?.useSoftDeletes,withTrashed=url.searchParams.get("withTrashed")==="true",onlyTrashed=url.searchParams.get("onlyTrashed")==="true";if(usesSoftDeletes&&!withTrashed)query=onlyTrashed?query.whereNotNull("deleted_at"):query.whereNull("deleted_at");const own=rowScoped?await resolveOwnership(model,await authedUserFromRequest(req),req):{enforced:!1,value:null,field:"",bypass:!1};if(own.enforced&&!own.bypass){if(own.value==null||Array.isArray(own.value)&&own.value.length===0){const emptyPaginator=buildIndexPaginator(url,page,perPage,0,!1,void 0);return new Response(JSON.stringify({data:[],...emptyPaginator}),{status:200,headers:{"Content-Type":"application/json","Cache-Control":"private, no-store",Vary:"Authorization"}})}query=Array.isArray(own.value)?query.where(own.field,"in",own.value):query.where(own.field,"=",own.value)}const rawResults=await query.limit(perPage+1).offset(offset).get()||[],hasMore=rawResults.length>perPage;let records=(hasMore?rawResults.slice(0,perPage):rawResults).map((r)=>stripHidden(applyReadCasts(r,model),hiddenFields));const includeParam=url.searchParams.get("include");if(includeParam)records=await applyIncludes(records,model,includeParam,models);const wantCount=url.searchParams.get("with_count")==="true";let total;if(wantCount)try{let countQuery=dynamicDb().selectFrom(table);if(own.enforced&&!own.bypass&&own.value!=null)countQuery=Array.isArray(own.value)?countQuery.where(own.field,"in",own.value):countQuery.where(own.field,"=",own.value);const raw=await countQuery.count(),n=typeof raw==="number"?raw:Number(raw?.count??raw);if(Number.isFinite(n))total=n}catch{}const respHeaders={"Content-Type":"application/json"};if(total!==void 0&&!Number.isNaN(total))respHeaders["X-Total-Count"]=String(total);respHeaders["Cache-Control"]=own.enforced&&!own.bypass?"private, no-store":"public, max-age=15, must-revalidate";respHeaders.Vary="Authorization";const paginator=buildIndexPaginator(url,page,perPage,records.length,hasMore,total);return new Response(JSON.stringify({data:records,...paginator}),{status:200,headers:respHeaders})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to fetch ${uri}`,detail:String(err)},500)}}),indexMiddleware);if(enabledRoutes.includes("show")&&!routeClaimed("GET",`${basePath}/{id}`))applyMiddleware(route.get(`${basePath}/{id}`,async(req)=>{try{const id=coerceId(req.params?.id);if(id==null)return jsonResponse({error:"Invalid ID parameter"},400);const result=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!result)return jsonResponse({error:`${modelName} not found`},404);const url=new URL(req.url);if(model.traits?.useSoftDeletes&&result.deleted_at&&url.searchParams.get("withTrashed")!=="true")return jsonResponse({error:`${modelName} not found`},404);const own=rowScoped?await resolveOwnership(model,await authedUserFromRequest(req),req):{enforced:!1,value:null,field:"",bypass:!1};if(own.enforced&&!own.bypass){if(own.value==null||!ownsRow(result[own.field],own.value))return jsonResponse({error:`${modelName} not found`},404)}const refusedView=await policyRefusal(modelName,"view",req,result);if(refusedView)return refusedView;let payload=stripHidden(applyReadCasts(result,model),hiddenFields);const includeParam=url.searchParams.get("include");if(includeParam){const[withRel]=await applyIncludes([payload],model,includeParam,models);payload=withRel}const lastWrite=payload?.updated_at||payload?.created_at,etag=lastWrite?`W/"${payload.id}-${String(lastWrite)}"`:void 0,ifNoneMatch=req.headers?.get?.("if-none-match");if(etag&&ifNoneMatch&&ifNoneMatch===etag)return new Response(null,{status:304,headers:{ETag:etag}});const headers={"Content-Type":"application/json"};if(etag)headers.ETag=etag;headers["Cache-Control"]=own.enforced&&!own.bypass?"private, no-store":"public, max-age=30, must-revalidate";headers.Vary="Authorization";return new Response(JSON.stringify({data:payload}),{status:200,headers})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to fetch ${modelName}`,detail:String(err)},500)}}),showMiddleware);if(!skipMutatingRoutes&&enabledRoutes.includes("store")&&!routeClaimed("POST",basePath))applyMiddleware(route.post(basePath,async(req)=>{try{const body=await getRequestBody(req),safeBody=dropHiddenInputs(body,hiddenFields);let data=filterFillable(safeBody,fillableFields);const authedUser=await authedUserFromRequest(req);if(authedUser){const stamped=await resolveAuthedFill(model,"creating",authedUser,req);for(const[k,v]of Object.entries(stamped))if(v!==void 0&&v!==null&&data[k]===void 0)data[k]=v}const refusedCreate=await policyRefusal(modelName,"create",req);if(refusedCreate)return refusedCreate;const own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);const stamped=stampOwnership(data,own.field,own.value);if(stamped.error)return jsonResponse({error:stamped.error},403);data=stamped.data}if(Object.keys(data).length===0)return jsonResponse({error:"No fillable fields provided"},422);const v=validateWriteBody(data,model,"creating");if(!v.valid)return jsonResponse({success:!1,message:"Validation failed",errors:v.errors},422);if(model.traits?.useTimestamps!==!1){const now=new Date().toISOString();data.created_at=now;data.updated_at=now}const hookedData=await applyDefinedSetters(data,model),writeData=toSnakeCaseKeys(applySetCasts(hookedData,model)),result=await dynamicDb().insertInto(table).values(writeData).execute();let created=writeData;try{const lastId=result?.lastInsertRowid??result?.insertId??result;if(lastId){const fetched=await dynamicDb().selectFrom(table).where({id:lastId}).executeTakeFirst();if(fetched)created=fetched}}catch{}return jsonResponse({data:stripHidden(applyReadCasts(created,model),hiddenFields)},201)}catch(err){const{status,body}=mapWriteError(err,modelName,"create");return jsonResponse(body,status)}}),abilities.store);if(!skipMutatingRoutes&&enabledRoutes.includes("update")){const updateHandler=async(req)=>{try{const id=coerceId(req.params?.id);if(id==null)return jsonResponse({error:"Invalid ID parameter"},400);const body=await getRequestBody(req),safeBody=dropHiddenInputs(body,hiddenFields),data=filterFillable(safeBody,fillableFields),authedUser=await authedUserFromRequest(req);if(authedUser){const stamped=await resolveAuthedFill(model,"updating",authedUser,req);for(const[k,v]of Object.entries(stamped))if(v!==void 0&&v!==null&&data[k]===void 0)data[k]=v}if(Object.keys(data).length===0)return jsonResponse({error:"No fillable fields provided"},422);const v=validateWriteBody(data,model,"updating");if(!v.valid)return jsonResponse({success:!1,message:"Validation failed",errors:v.errors},422);const existing=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!existing)return jsonResponse({error:`${modelName} not found`},404);const refusedUpdate=await policyRefusal(modelName,"update",req,existing);if(refusedUpdate)return refusedUpdate;const own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);const rowOwner=existing[own.field];if(!ownsRow(rowOwner,own.value))return jsonResponse({error:`Not your ${modelName}`},403);const submittedOwnerKey=Object.keys(data).find((k)=>toSnakeCase(k)===toSnakeCase(own.field));if(submittedOwnerKey!==void 0&&!ownsRow(data[submittedOwnerKey],own.value))return jsonResponse({error:`Cannot reassign ${modelName} ownership`},403)}if(model.traits?.useTimestamps!==!1)data.updated_at=new Date().toISOString();const hookedData=await applyDefinedSetters(data,model),writeData=toSnakeCaseKeys(applySetCasts(hookedData,model));await dynamicDb().updateTable(table).set(writeData).where({id}).execute();let updated={...existing,...writeData};try{const fetched=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(fetched)updated=fetched}catch{}return jsonResponse({data:stripHidden(applyReadCasts(updated,model),hiddenFields)})}catch(err){const{status,body}=mapWriteError(err,modelName,"update");return jsonResponse(body,status)}};if(!routeClaimed("PUT",`${basePath}/{id}`))applyMiddleware(route.put(`${basePath}/{id}`,updateHandler),abilities.update);if(!routeClaimed("PATCH",`${basePath}/{id}`))applyMiddleware(route.patch(`${basePath}/{id}`,updateHandler),abilities.update)}const usesSoftDeletes=!!model.traits?.useSoftDeletes;if(!skipMutatingRoutes&&enabledRoutes.includes("destroy")&&!routeClaimed("DELETE",`${basePath}/{id}`))applyMiddleware(route.delete(`${basePath}/{id}`,async(req)=>{try{const id=coerceId(req.params?.id);if(id==null)return jsonResponse({error:"Invalid ID parameter"},400);const existing=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!existing)return jsonResponse({error:`${modelName} not found`},404);const authedUser=await authedUserFromRequest(req),own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);const rowOwner=existing[own.field];if(!ownsRow(rowOwner,own.value))return jsonResponse({error:`Not your ${modelName}`},403)}const refusedDelete=await policyRefusal(modelName,"delete",req,existing);if(refusedDelete)return refusedDelete;if(usesSoftDeletes){const now=new Date().toISOString();await dynamicDb().updateTable(table).set({deleted_at:now,updated_at:now}).where({id}).execute()}else await dynamicDb().deleteFrom(table).where({id}).execute();return new Response(null,{status:204})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to delete ${modelName}`,detail:String(err)},500)}}),abilities.destroy);if(!skipMutatingRoutes&&enabledRoutes.includes("destroy")&&!routeClaimed("POST",`${basePath}/bulk-delete`))applyMiddleware(route.post(`${basePath}/bulk-delete`,async(req)=>{try{const ids=(await getRequestBody(req))?.ids;if(!Array.isArray(ids)||ids.length===0)return jsonResponse({error:"An array of IDs is required"},422);if(ids.length>100)return jsonResponse({error:"Cannot delete more than 100 records at once"},422);const validIds=[],invalidIds=[];for(const raw of ids){const coerced=coerceId(raw);if(coerced!=null)validIds.push(coerced);else invalidIds.push(raw)}if(invalidIds.length>0)return jsonResponse({error:"Invalid IDs in batch",invalid:invalidIds},422);const authedUser=await authedUserFromRequest(req),own=await resolveOwnership(model,authedUser,req);if(own.enforced&&!own.bypass){if(!authedUser)return jsonResponse({error:"Auth required"},401);if(own.value==null)return jsonResponse({error:"Caller has no ownership identity"},403);const rows=await dynamicDb().selectFrom(table).select(["id",own.field]).execute(),ownedIds=new Set(rows.filter((r)=>ownsRow(r[own.field],own.value)).map((r)=>String(r.id))),notOwned=validIds.filter((id)=>!ownedIds.has(String(id)));if(notOwned.length>0)return jsonResponse({error:`Cannot delete ${modelName} you don't own`,ids:notOwned},403)}if(await hasPolicyFor(modelName)){const refusedRows=[];for(const id of validIds){const row=await dynamicDb().selectFrom(table).where({id}).executeTakeFirst();if(!row)continue;if(await policyRefusal(modelName,"delete",req,row))refusedRows.push(id)}if(refusedRows.length>0)return jsonResponse({error:`Cannot delete ${modelName} this policy refuses`,ids:refusedRows},403)}const now=new Date().toISOString();for(const id of validIds)if(usesSoftDeletes)await dynamicDb().updateTable(table).set({deleted_at:now,updated_at:now}).where({id}).execute();else await dynamicDb().deleteFrom(table).where({id}).execute();return jsonResponse({message:`Successfully deleted ${validIds.length} ${uri}`})}catch(err){if(err instanceof HttpError){const body={error:err.message};if(err.details!==void 0)body.details=err.details;return jsonResponse(body,err.status||400)}return jsonResponse({error:`Failed to bulk delete ${uri}`,detail:String(err)},500)}}),abilities.destroy)}{const report=describeUnscopedMutatingModels(unscopedMutatingModels,rowScopingPolicy);if(report)log.warn(report);const contradictions=describeContradictoryRoleGates(contradictoryRoleGates);if(contradictions)log.warn(contradictions)}export default route;
|
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.
|
|
5
|
+
"version": "0.75.249",
|
|
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.
|
|
67
|
-
"@stacksjs/cli": "0.75.
|
|
68
|
-
"@stacksjs/config": "0.75.
|
|
69
|
-
"@stacksjs/database": "0.75.
|
|
70
|
-
"@stacksjs/env": "0.75.
|
|
71
|
-
"@stacksjs/error-handling": "0.75.
|
|
72
|
-
"@stacksjs/events": "0.75.
|
|
73
|
-
"@stacksjs/logging": "0.75.
|
|
74
|
-
"@stacksjs/model-meta": "0.75.
|
|
75
|
-
"@stacksjs/pagination": "0.75.
|
|
76
|
-
"@stacksjs/path": "0.75.
|
|
77
|
-
"@stacksjs/query-builder": "0.75.
|
|
78
|
-
"@stacksjs/realtime": "0.75.
|
|
79
|
-
"@stacksjs/router": "0.75.
|
|
80
|
-
"@stacksjs/storage": "0.75.
|
|
81
|
-
"@stacksjs/strings": "0.75.
|
|
66
|
+
"@stacksjs/cache": "0.75.249",
|
|
67
|
+
"@stacksjs/cli": "0.75.249",
|
|
68
|
+
"@stacksjs/config": "0.75.249",
|
|
69
|
+
"@stacksjs/database": "0.75.249",
|
|
70
|
+
"@stacksjs/env": "0.75.249",
|
|
71
|
+
"@stacksjs/error-handling": "0.75.249",
|
|
72
|
+
"@stacksjs/events": "0.75.249",
|
|
73
|
+
"@stacksjs/logging": "0.75.249",
|
|
74
|
+
"@stacksjs/model-meta": "0.75.249",
|
|
75
|
+
"@stacksjs/pagination": "0.75.249",
|
|
76
|
+
"@stacksjs/path": "0.75.249",
|
|
77
|
+
"@stacksjs/query-builder": "0.75.249",
|
|
78
|
+
"@stacksjs/realtime": "0.75.249",
|
|
79
|
+
"@stacksjs/router": "0.75.249",
|
|
80
|
+
"@stacksjs/storage": "0.75.249",
|
|
81
|
+
"@stacksjs/strings": "0.75.249",
|
|
82
82
|
"@stacksjs/ts-validation": "^0.5.6",
|
|
83
|
-
"@stacksjs/validation": "0.75.
|
|
83
|
+
"@stacksjs/validation": "0.75.249",
|
|
84
84
|
"bun-query-builder": "^0.3.8"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
|
-
"@stacksjs/auth": "0.75.
|
|
88
|
-
"@stacksjs/payments": "0.75.
|
|
89
|
-
"@stacksjs/queue": "0.75.
|
|
90
|
-
"@stacksjs/search-engine": "0.75.
|
|
87
|
+
"@stacksjs/auth": "0.75.249",
|
|
88
|
+
"@stacksjs/payments": "0.75.249",
|
|
89
|
+
"@stacksjs/queue": "0.75.249",
|
|
90
|
+
"@stacksjs/search-engine": "0.75.249",
|
|
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.
|
|
111
|
+
"@stacksjs/types": "0.75.249",
|
|
112
112
|
"better-dx": "^0.2.24",
|
|
113
113
|
"stripe": "23.0.0"
|
|
114
114
|
}
|