@stacksjs/orm 0.75.467 → 0.75.473

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.d.ts CHANGED
@@ -277,6 +277,19 @@ export declare interface TaggableTable {
277
277
  created_at?: string
278
278
  updated_at?: string
279
279
  }
280
+ /**
281
+ * The error a call on an unavailable model raises, instead of `undefined`.
282
+ *
283
+ * Its own class, so a caller can tell "this feature is off" from a database
284
+ * that failed: the dashboard answered both with the same 503 (stacksjs/stacks#3184).
285
+ * `name` is the check across module instances, where `instanceof` can miss.
286
+ */
287
+ export declare class ModelUnavailableError extends Error {
288
+ readonly name: string;
289
+ readonly model: string;
290
+ readonly feature: string | undefined;
291
+ constructor(model: string, feature: string | undefined);
292
+ }
280
293
  export * from './utils/prunable';
281
294
  export {
282
295
  collectEncryptedAttributes,
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";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]}export function frameworkModelFeature(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]}export function frameworkModelFeature(name){return FRAMEWORK_MODEL_MANIFEST.find(([model])=>model===name)?.[2]}export class ModelUnavailableError extends Error{model;feature;name="ModelUnavailableError";constructor(model,feature){super(`Model "${model}" is not available in this process. ${feature?`The '${feature}' feature is off, so this model was never loaded - set \`enabled: true\` in config/${feature}.ts to use it.`:"No model file for it was found under app/Models or the framework defaults."}`);this.model=model;this.feature=feature}}function unavailableModel(name){return new ModelUnavailableError(name,featureGateFor(name))}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";
@@ -15,6 +15,7 @@ export declare interface MessagingOptions {
15
15
  broadcast?: boolean
16
16
  onMessageSent?: (event: { message: DirectMessage, actorId: number, recipientId: number, scope: string, participantType: string }) => void | Promise<void>
17
17
  authorize: (actorId: number, recipientId: number) => boolean | Promise<boolean>
18
+ changeWindowMs?: number
18
19
  }
19
20
  export declare interface DirectConversation {
20
21
  id: string
@@ -47,12 +48,16 @@ export declare interface DirectMessage {
47
48
  client_key: string
48
49
  created_at: string
49
50
  read_at: string | null
51
+ edited_at: string | null
52
+ deleted_at: string | null
50
53
  }
51
54
  export declare interface Messenger {
52
55
  direct: (recipientId: number) => Promise<{ id: string, recipient_id: number }>
53
56
  list: () => Promise<DirectConversation[]>
54
57
  messages: (id: string, before?: number) => Promise<{ messages: DirectMessage[], has_more: boolean }>
55
58
  send: (id: string, body: string, clientKey: string, attachments?: DirectMessageAttachment[]) => Promise<DirectMessage>
59
+ edit: (id: string, messageId: number, body: string) => Promise<DirectMessage>
60
+ unsend: (id: string, messageId: number) => Promise<DirectMessage>
56
61
  markRead: (id: string, throughId: number) => Promise<void>
57
62
  }
58
63
  export declare class MessagingError extends Error {
package/dist/messaging.js CHANGED
@@ -1 +1 @@
1
- import{createHash}from"node:crypto";import{raw}from"bun-query-builder";import{db,enqueueAfterCommit,parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";import{log}from"@stacksjs/logging";import{emit,getServer}from"@stacksjs/realtime";export class MessagingError extends Error{status;constructor(message,status){super(message);this.status=status}}export function messagingChannel(participantType,userId){return`private-messaging.${createHash("sha256").update(participantType).digest("hex")}.${positive(userId)}`}const MAX_ATTACHMENTS=10,MAX_ATTACHMENTS_LENGTH=4000;function positive(value){if(!Number.isSafeInteger(value)||Number(value)<=0)throw new MessagingError("A positive integer is required",422);return Number(value)}function duplicate(error){const e=error;return e.code==="23505"||e.code==="SQLITE_CONSTRAINT_PRIMARYKEY"||e.code==="SQLITE_CONSTRAINT_UNIQUE"||e.code==="SQLITE_CONSTRAINT"||e.errno===1062}function whole(value,name){if(!Number.isSafeInteger(value)||Number(value)<0)throw new MessagingError(`An attachment ${name} is a whole number of 0 or more`,422);return Number(value)}const ATTACHMENT_KEYS=new Set(["file","type","name","size","width","height"]);function text(value,name){if(typeof value!=="string"||!value.trim()||value.length>255)throw new MessagingError(`An attachment ${name} is 1 to 255 characters`,422);return value.trim()}function attachmentsOf(value){if(value===void 0||value===null)return[];if(!Array.isArray(value))throw new MessagingError("Attachments are a list",422);if(value.length>MAX_ATTACHMENTS)throw new MessagingError(`Send at most ${MAX_ATTACHMENTS} attachments`,422);const list=value.map((entry)=>{const item=entry??{},file=typeof item.file==="string"?item.file.trim():"";if(!file||file.length>255)throw new MessagingError("Each attachment needs a file of 1 to 255 characters",422);const unknown=Object.keys(item).find((key)=>!ATTACHMENT_KEYS.has(key));if(unknown!==void 0)throw new MessagingError(`An attachment cannot carry \`${unknown.slice(0,64)}\`; it may carry ${[...ATTACHMENT_KEYS].join(", ")}`,422);const out={file};if(item.type!==void 0)out.type=text(item.type,"type");if(item.name!==void 0)out.name=text(item.name,"name");if(item.size!==void 0)out.size=whole(item.size,"size");if(item.width!==void 0)out.width=whole(item.width,"width");if(item.height!==void 0)out.height=whole(item.height,"height");return out});if(JSON.stringify(list).length>MAX_ATTACHMENTS_LENGTH)throw new MessagingError("Those attachments are too long to record",422);return list}function storedAttachments(value){if(typeof value!=="string"||!value)return[];try{const parsed=JSON.parse(value);return Array.isArray(parsed)?parsed:[]}catch{return[]}}function message(row){return{id:Number(row.id),conversation_id:String(row.conversation_id),sender_id:Number(row.sender_id),body:String(row.body),attachments:storedAttachments(row.attachments),client_key:String(row.client_key),created_at:parseSqlDateTime(row.created_at).toISOString(),read_at:parseSqlDateTime(row.read_at)?.toISOString()??null}}export function createMessenger(options){const actorId=positive(options.actorId),participantType=options.participantType??"users",maxLength=options.maxLength??4000,pageSize=options.pageSize??50;if(!options.scope||options.scope.length>255||!participantType||participantType.length>255||typeof options.authorize!=="function"||!Number.isSafeInteger(maxLength)||maxLength<1||maxLength>1e5||!Number.isSafeInteger(pageSize)||pageSize<1||pageSize>100)throw new MessagingError("Invalid messaging configuration",422);async function authorize(recipientId){if(options.enabled===!1)throw new MessagingError("Messaging is disabled",403);if(recipientId===actorId||!await options.authorize(actorId,recipientId))throw new MessagingError("Conversation not found",404)}async function conversation(id){if(typeof id!=="string"||!/^[a-f0-9]{64}$/.test(id))throw new MessagingError("Conversation not found",404);const row=await db.selectFrom("chat_conversations").selectAll().where("id","=",id).where("scope","=",options.scope).where("participant_type","=",participantType).executeTakeFirst();if(!row||Number(row.first_id)!==actorId&&Number(row.second_id)!==actorId)throw new MessagingError("Conversation not found",404);const recipientId=Number(row.first_id)===actorId?Number(row.second_id):Number(row.first_id);await authorize(recipientId);return recipientId}async function direct(recipientId){positive(recipientId);await authorize(recipientId);const[first,second]=[actorId,recipientId].sort((a,b)=>a-b),id=createHash("sha256").update(JSON.stringify([options.scope,participantType,first,second])).digest("hex");if(!await db.selectFrom("chat_conversations").select("id").where("id","=",id).executeTakeFirst())try{await db.insertInto("chat_conversations").values({id,scope:options.scope,participant_type:participantType,first_id:first,second_id:second,created_at:sqlDateTime()}).execute()}catch(error){if(!duplicate(error))throw error}return{id,recipient_id:recipientId}}async function list(){if(options.enabled===!1)throw new MessagingError("Messaging is disabled",403);const query=()=>db.selectFrom("chat_conversations").selectAll().where("scope","=",options.scope).where("participant_type","=",participantType),threads=[...await query().where("first_id","=",actorId).execute(),...await query().where("second_id","=",actorId).execute()].map((row)=>({id:String(row.id),recipientId:Number(row.first_id)===actorId?Number(row.second_id):Number(row.first_id)})),allowed=await Promise.all(threads.map((thread)=>options.authorize(actorId,thread.recipientId))),visible=threads.filter((_,index)=>allowed[index]);if(!visible.length)return[];const ids=visible.map((thread)=>thread.id),latestIds=(await db.selectFrom("chat_messages").select(["conversation_id"]).selectRaw(raw`MAX(id) AS id`).where("conversation_id","in",ids).groupBy("conversation_id").execute()).map((row)=>Number(row.id)).filter((id)=>Number.isSafeInteger(id)&&id>0),latest=latestIds.length?await db.selectFrom("chat_messages").selectAll().where("id","in",latestIds).execute():[],latestByThread=new Map(latest.map((row)=>[String(row.conversation_id),row])),recipientOf=new Map(visible.map((thread)=>[thread.id,thread.recipientId])),unreadRows=await db.selectFrom("chat_messages").select(["conversation_id","sender_id"]).selectRaw(raw`COUNT(*) AS unread`).where("conversation_id","in",ids).whereNull("read_at").groupBy("conversation_id","sender_id").execute(),unread=new Map;for(const row of unreadRows){const thread=String(row.conversation_id);if(Number(row.sender_id)===recipientOf.get(thread))unread.set(thread,Number(row.unread))}return visible.map((thread)=>{const row=latestByThread.get(thread.id);return{id:thread.id,recipient_id:thread.recipientId,last_message:row?message(row):null,unread:unread.get(thread.id)??0}}).sort((a,b)=>(b.last_message?.id??0)-(a.last_message?.id??0))}async function messages(id,before){await conversation(id);let query=db.selectFrom("chat_messages").selectAll().where("conversation_id","=",id);if(before!==void 0)query=query.where("id","<",positive(before));const rows=await query.orderBy("id","desc").limit(pageSize+1).execute();return{messages:rows.slice(0,pageSize).reverse().map(message),has_more:rows.length>pageSize}}async function send(id,body,clientKey,attachments){const recipientId=await conversation(id),files=attachmentsOf(attachments),text=typeof body==="string"?body.trim():"";if(typeof body!=="string"||!text&&!files.length||text.length>maxLength)throw new MessagingError(`Write a message of 1 to ${maxLength} characters, or send an attachment`,422);if(typeof clientKey!=="string"||!/^[\w-]{1,100}$/.test(clientKey))throw new MessagingError("A valid message retry key is required",422);const stored=files.length?JSON.stringify(files):null,lookup=()=>db.selectFrom("chat_messages").selectAll().where("conversation_id","=",id).where("sender_id","=",actorId).where("client_key","=",clientKey).executeTakeFirst();let row=await lookup(),inserted=!1;if(!row){try{await db.insertInto("chat_messages").values({conversation_id:id,sender_id:actorId,body:text,client_key:clientKey,created_at:sqlDateTime(),read_at:null,...stored?{attachments:stored}:{}}).execute();inserted=!0}catch(error){if(!duplicate(error))throw error}row=await lookup()}if(!row)throw Error("Message could not be saved");if(row.body!==text||String(row.attachments??"")!==String(stored??""))throw new MessagingError("This retry key belongs to a different message",409);const saved=message(row);if(inserted){const deliver=async()=>{notify(recipientId,id,"messaging.sent");try{await options.onMessageSent?.({message:saved,actorId,recipientId,scope:options.scope,participantType})}catch(error){log.warn(`Messaging notification failed: ${error instanceof Error?error.message:"Unknown error"}`)}};if(!enqueueAfterCommit(deliver))await deliver()}return saved}async function markRead(id,throughId){const recipientId=await conversation(id);positive(throughId);if(!await db.selectFrom("chat_messages").select("id").where("conversation_id","=",id).where("id","=",throughId).executeTakeFirst())throw new MessagingError("Message not found",404);const unread=await db.selectFrom("chat_messages").select("id").where("conversation_id","=",id).where("sender_id","=",recipientId).where("id","<=",throughId).whereNull("read_at").limit(1).executeTakeFirst();await db.updateTable("chat_messages").set({read_at:sqlDateTime()}).where("conversation_id","=",id).where("sender_id","=",recipientId).where("id","<=",throughId).whereNull("read_at").execute();if(unread)notify(recipientId,id,"messaging.read")}function notify(recipientId,id,event){if(options.broadcast===!1||!getServer())return;for(const userId of[actorId,recipientId])emit(messagingChannel(participantType,userId),event,{conversation_id:id})}return{direct,list,messages,send,markRead}}export function createMessagingMethods(participantType){return{messenger:(id,options)=>createMessenger({...options,actorId:id,participantType})}}
1
+ import{createHash}from"node:crypto";import{raw}from"bun-query-builder";import{db,enqueueAfterCommit,parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";import{log}from"@stacksjs/logging";import{emit,getServer}from"@stacksjs/realtime";export class MessagingError extends Error{status;constructor(message,status){super(message);this.status=status}}export function messagingChannel(participantType,userId){return`private-messaging.${createHash("sha256").update(participantType).digest("hex")}.${positive(userId)}`}const MAX_ATTACHMENTS=10,MAX_ATTACHMENTS_LENGTH=4000;function positive(value){if(!Number.isSafeInteger(value)||Number(value)<=0)throw new MessagingError("A positive integer is required",422);return Number(value)}function duplicate(error){const e=error;return e.code==="23505"||e.code==="SQLITE_CONSTRAINT_PRIMARYKEY"||e.code==="SQLITE_CONSTRAINT_UNIQUE"||e.code==="SQLITE_CONSTRAINT"||e.errno===1062}function whole(value,name){if(!Number.isSafeInteger(value)||Number(value)<0)throw new MessagingError(`An attachment ${name} is a whole number of 0 or more`,422);return Number(value)}const ATTACHMENT_KEYS=new Set(["file","type","name","size","width","height"]);function text(value,name){if(typeof value!=="string"||!value.trim()||value.length>255)throw new MessagingError(`An attachment ${name} is 1 to 255 characters`,422);return value.trim()}function attachmentsOf(value){if(value===void 0||value===null)return[];if(!Array.isArray(value))throw new MessagingError("Attachments are a list",422);if(value.length>MAX_ATTACHMENTS)throw new MessagingError(`Send at most ${MAX_ATTACHMENTS} attachments`,422);const list=value.map((entry)=>{const item=entry??{},file=typeof item.file==="string"?item.file.trim():"";if(!file||file.length>255)throw new MessagingError("Each attachment needs a file of 1 to 255 characters",422);const unknown=Object.keys(item).find((key)=>!ATTACHMENT_KEYS.has(key));if(unknown!==void 0)throw new MessagingError(`An attachment cannot carry \`${unknown.slice(0,64)}\`; it may carry ${[...ATTACHMENT_KEYS].join(", ")}`,422);const out={file};if(item.type!==void 0)out.type=text(item.type,"type");if(item.name!==void 0)out.name=text(item.name,"name");if(item.size!==void 0)out.size=whole(item.size,"size");if(item.width!==void 0)out.width=whole(item.width,"width");if(item.height!==void 0)out.height=whole(item.height,"height");return out});if(JSON.stringify(list).length>MAX_ATTACHMENTS_LENGTH)throw new MessagingError("Those attachments are too long to record",422);return list}function storedAttachments(value){if(typeof value!=="string"||!value)return[];try{const parsed=JSON.parse(value);return Array.isArray(parsed)?parsed:[]}catch{return[]}}function message(row){return{id:Number(row.id),conversation_id:String(row.conversation_id),sender_id:Number(row.sender_id),body:String(row.body),attachments:storedAttachments(row.attachments),client_key:String(row.client_key),created_at:parseSqlDateTime(row.created_at).toISOString(),read_at:parseSqlDateTime(row.read_at)?.toISOString()??null,edited_at:parseSqlDateTime(row.edited_at)?.toISOString()??null,deleted_at:parseSqlDateTime(row.deleted_at)?.toISOString()??null}}export function createMessenger(options){const actorId=positive(options.actorId),participantType=options.participantType??"users",maxLength=options.maxLength??4000,pageSize=options.pageSize??50,changeWindowMs=options.changeWindowMs;if(!options.scope||options.scope.length>255||!participantType||participantType.length>255||typeof options.authorize!=="function"||!Number.isSafeInteger(maxLength)||maxLength<1||maxLength>1e5||!Number.isSafeInteger(pageSize)||pageSize<1||pageSize>100)throw new MessagingError("Invalid messaging configuration",422);async function authorize(recipientId){if(options.enabled===!1)throw new MessagingError("Messaging is disabled",403);if(recipientId===actorId||!await options.authorize(actorId,recipientId))throw new MessagingError("Conversation not found",404)}async function conversation(id){if(typeof id!=="string"||!/^[a-f0-9]{64}$/.test(id))throw new MessagingError("Conversation not found",404);const row=await db.selectFrom("chat_conversations").selectAll().where("id","=",id).where("scope","=",options.scope).where("participant_type","=",participantType).executeTakeFirst();if(!row||Number(row.first_id)!==actorId&&Number(row.second_id)!==actorId)throw new MessagingError("Conversation not found",404);const recipientId=Number(row.first_id)===actorId?Number(row.second_id):Number(row.first_id);await authorize(recipientId);return recipientId}async function direct(recipientId){positive(recipientId);await authorize(recipientId);const[first,second]=[actorId,recipientId].sort((a,b)=>a-b),id=createHash("sha256").update(JSON.stringify([options.scope,participantType,first,second])).digest("hex");if(!await db.selectFrom("chat_conversations").select("id").where("id","=",id).executeTakeFirst())try{await db.insertInto("chat_conversations").values({id,scope:options.scope,participant_type:participantType,first_id:first,second_id:second,created_at:sqlDateTime()}).execute()}catch(error){if(!duplicate(error))throw error}return{id,recipient_id:recipientId}}async function list(){if(options.enabled===!1)throw new MessagingError("Messaging is disabled",403);const query=()=>db.selectFrom("chat_conversations").selectAll().where("scope","=",options.scope).where("participant_type","=",participantType),threads=[...await query().where("first_id","=",actorId).execute(),...await query().where("second_id","=",actorId).execute()].map((row)=>({id:String(row.id),recipientId:Number(row.first_id)===actorId?Number(row.second_id):Number(row.first_id)})),allowed=await Promise.all(threads.map((thread)=>options.authorize(actorId,thread.recipientId))),visible=threads.filter((_,index)=>allowed[index]);if(!visible.length)return[];const ids=visible.map((thread)=>thread.id),latestIds=(await db.selectFrom("chat_messages").select(["conversation_id"]).selectRaw(raw`MAX(id) AS id`).where("conversation_id","in",ids).groupBy("conversation_id").execute()).map((row)=>Number(row.id)).filter((id)=>Number.isSafeInteger(id)&&id>0),latest=latestIds.length?await db.selectFrom("chat_messages").selectAll().where("id","in",latestIds).execute():[],latestByThread=new Map(latest.map((row)=>[String(row.conversation_id),row])),recipientOf=new Map(visible.map((thread)=>[thread.id,thread.recipientId])),unreadRows=await db.selectFrom("chat_messages").select(["conversation_id","sender_id"]).selectRaw(raw`COUNT(*) AS unread`).where("conversation_id","in",ids).whereNull("read_at").groupBy("conversation_id","sender_id").execute(),unread=new Map;for(const row of unreadRows){const thread=String(row.conversation_id);if(Number(row.sender_id)===recipientOf.get(thread))unread.set(thread,Number(row.unread))}return visible.map((thread)=>{const row=latestByThread.get(thread.id);return{id:thread.id,recipient_id:thread.recipientId,last_message:row?message(row):null,unread:unread.get(thread.id)??0}}).sort((a,b)=>(b.last_message?.id??0)-(a.last_message?.id??0))}async function messages(id,before){await conversation(id);let query=db.selectFrom("chat_messages").selectAll().where("conversation_id","=",id);if(before!==void 0)query=query.where("id","<",positive(before));const rows=await query.orderBy("id","desc").limit(pageSize+1).execute();return{messages:rows.slice(0,pageSize).reverse().map(message),has_more:rows.length>pageSize}}async function send(id,body,clientKey,attachments){const recipientId=await conversation(id),files=attachmentsOf(attachments),text=typeof body==="string"?body.trim():"";if(typeof body!=="string"||!text&&!files.length||text.length>maxLength)throw new MessagingError(`Write a message of 1 to ${maxLength} characters, or send an attachment`,422);if(typeof clientKey!=="string"||!/^[\w-]{1,100}$/.test(clientKey))throw new MessagingError("A valid message retry key is required",422);const stored=files.length?JSON.stringify(files):null,lookup=()=>db.selectFrom("chat_messages").selectAll().where("conversation_id","=",id).where("sender_id","=",actorId).where("client_key","=",clientKey).executeTakeFirst();let row=await lookup(),inserted=!1;if(!row){try{await db.insertInto("chat_messages").values({conversation_id:id,sender_id:actorId,body:text,client_key:clientKey,created_at:sqlDateTime(),read_at:null,...stored?{attachments:stored}:{}}).execute();inserted=!0}catch(error){if(!duplicate(error))throw error}row=await lookup()}if(!row)throw Error("Message could not be saved");if(row.body!==text||String(row.attachments??"")!==String(stored??""))throw new MessagingError("This retry key belongs to a different message",409);const saved=message(row);if(inserted){const deliver=async()=>{notify(recipientId,id,"messaging.sent");try{await options.onMessageSent?.({message:saved,actorId,recipientId,scope:options.scope,participantType})}catch(error){log.warn(`Messaging notification failed: ${error instanceof Error?error.message:"Unknown error"}`)}};if(!enqueueAfterCommit(deliver))await deliver()}return saved}async function ownMessage(id,messageId){const recipientId=await conversation(id);positive(messageId);const row=await db.selectFrom("chat_messages").selectAll().where("conversation_id","=",id).where("id","=",messageId).executeTakeFirst();if(!row||Number(row.sender_id)!==actorId)throw new MessagingError("Message not found",404);if(row.deleted_at)throw new MessagingError("This message was unsent",409);if(changeWindowMs!==void 0){const sent=parseSqlDateTime(row.created_at)?.getTime();if(sent===void 0||Date.now()-sent>changeWindowMs)throw new MessagingError("This message can no longer be changed",409)}return{row,recipientId}}async function edit(id,messageId,body){const{row,recipientId}=await ownMessage(id,messageId),text=typeof body==="string"?body.trim():"";if(typeof body!=="string"||!text||text.length>maxLength)throw new MessagingError(`Write a message of 1 to ${maxLength} characters`,422);if(text===String(row.body))return message(row);await db.updateTable("chat_messages").set({body:text,edited_at:sqlDateTime()}).where("id","=",Number(row.id)).execute();const saved=await db.selectFrom("chat_messages").selectAll().where("id","=",Number(row.id)).executeTakeFirst();notify(recipientId,id,"messaging.changed");return message(saved??row)}async function unsend(id,messageId){const{row,recipientId}=await ownMessage(id,messageId);await db.updateTable("chat_messages").set({body:"",attachments:null,deleted_at:sqlDateTime()}).where("id","=",Number(row.id)).execute();const saved=await db.selectFrom("chat_messages").selectAll().where("id","=",Number(row.id)).executeTakeFirst();notify(recipientId,id,"messaging.changed");return message(saved??row)}async function markRead(id,throughId){const recipientId=await conversation(id);positive(throughId);if(!await db.selectFrom("chat_messages").select("id").where("conversation_id","=",id).where("id","=",throughId).executeTakeFirst())throw new MessagingError("Message not found",404);const unread=await db.selectFrom("chat_messages").select("id").where("conversation_id","=",id).where("sender_id","=",recipientId).where("id","<=",throughId).whereNull("read_at").limit(1).executeTakeFirst();await db.updateTable("chat_messages").set({read_at:sqlDateTime()}).where("conversation_id","=",id).where("sender_id","=",recipientId).where("id","<=",throughId).whereNull("read_at").execute();if(unread)notify(recipientId,id,"messaging.read")}function notify(recipientId,id,event){if(options.broadcast===!1||!getServer())return;for(const userId of[actorId,recipientId])emit(messagingChannel(participantType,userId),event,{conversation_id:id})}return{direct,list,messages,send,edit,unsend,markRead}}export function createMessagingMethods(participantType){return{messenger:(id,options)=>createMessenger({...options,actorId:id,participantType})}}
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.467",
5
+ "version": "0.75.473",
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.467",
67
- "@stacksjs/cli": "0.75.467",
68
- "@stacksjs/config": "0.75.467",
69
- "@stacksjs/database": "0.75.467",
70
- "@stacksjs/env": "0.75.467",
71
- "@stacksjs/error-handling": "0.75.467",
72
- "@stacksjs/events": "0.75.467",
73
- "@stacksjs/logging": "0.75.467",
74
- "@stacksjs/model-meta": "0.75.467",
75
- "@stacksjs/pagination": "0.75.467",
76
- "@stacksjs/path": "0.75.467",
77
- "@stacksjs/query-builder": "0.75.467",
78
- "@stacksjs/realtime": "0.75.467",
79
- "@stacksjs/router": "0.75.467",
80
- "@stacksjs/storage": "0.75.467",
81
- "@stacksjs/strings": "0.75.467",
66
+ "@stacksjs/cache": "0.75.473",
67
+ "@stacksjs/cli": "0.75.473",
68
+ "@stacksjs/config": "0.75.473",
69
+ "@stacksjs/database": "0.75.473",
70
+ "@stacksjs/env": "0.75.473",
71
+ "@stacksjs/error-handling": "0.75.473",
72
+ "@stacksjs/events": "0.75.473",
73
+ "@stacksjs/logging": "0.75.473",
74
+ "@stacksjs/model-meta": "0.75.473",
75
+ "@stacksjs/pagination": "0.75.473",
76
+ "@stacksjs/path": "0.75.473",
77
+ "@stacksjs/query-builder": "0.75.473",
78
+ "@stacksjs/realtime": "0.75.473",
79
+ "@stacksjs/router": "0.75.473",
80
+ "@stacksjs/storage": "0.75.473",
81
+ "@stacksjs/strings": "0.75.473",
82
82
  "@stacksjs/ts-validation": "^0.5.7",
83
- "@stacksjs/validation": "0.75.467",
83
+ "@stacksjs/validation": "0.75.473",
84
84
  "bun-query-builder": "^0.3.38"
85
85
  },
86
86
  "peerDependencies": {
87
- "@stacksjs/auth": "0.75.467",
88
- "@stacksjs/payments": "0.75.467",
89
- "@stacksjs/queue": "0.75.467",
90
- "@stacksjs/search-engine": "0.75.467",
87
+ "@stacksjs/auth": "0.75.473",
88
+ "@stacksjs/payments": "0.75.473",
89
+ "@stacksjs/queue": "0.75.473",
90
+ "@stacksjs/search-engine": "0.75.473",
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.467",
111
+ "@stacksjs/types": "0.75.473",
112
112
  "better-dx": "^0.2.24",
113
113
  "stripe": "23.0.0"
114
114
  }