@stacksjs/orm 0.75.121 → 0.75.122

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.
@@ -1,3 +1,5 @@
1
+ /** Private per-person inbox, isolated from other model participant types. */
2
+ export declare function messagingChannel(participantType: string, userId: number): string;
1
3
  declare function message(row: Record<string, unknown>): DirectMessage;
2
4
  /** Direct conversations with persistence, tenant isolation, receipts and safe retries. */
3
5
  export declare function createMessenger(options: MessagingOptions): Messenger;
@@ -10,6 +12,7 @@ export declare interface MessagingOptions {
10
12
  enabled?: boolean
11
13
  maxLength?: number
12
14
  pageSize?: number
15
+ broadcast?: boolean
13
16
  authorize: (actorId: number, recipientId: number) => boolean | Promise<boolean>
14
17
  }
15
18
  export declare interface DirectConversation {
package/dist/messaging.js CHANGED
@@ -1 +1 @@
1
- import{createHash}from"node:crypto";import{db,parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";export class MessagingError extends Error{status;constructor(message,status){super(message);this.status=status}}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 message(row){return{id:Number(row.id),conversation_id:String(row.conversation_id),sender_id:Number(row.sender_id),body:String(row.body),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),rows=[...await query().where("first_id","=",actorId).execute(),...await query().where("second_id","=",actorId).execute()],results=[];for(const row of rows){const recipientId=Number(row.first_id)===actorId?Number(row.second_id):Number(row.first_id);if(!await options.authorize(actorId,recipientId))continue;const latest=await db.selectFrom("chat_messages").selectAll().where("conversation_id","=",row.id).orderBy("id","desc").limit(1).executeTakeFirst(),unread=await db.selectFrom("chat_messages").select("id").where("conversation_id","=",row.id).where("sender_id","=",recipientId).whereNull("read_at").count();results.push({id:String(row.id),recipient_id:recipientId,last_message:latest?message(latest):null,unread:Number(unread)})}return results.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){await conversation(id);if(typeof body!=="string"||!body.trim()||body.trim().length>maxLength)throw new MessagingError(`Write a message of 1 to ${maxLength} characters`,422);if(typeof clientKey!=="string"||!/^[\w-]{1,100}$/.test(clientKey))throw new MessagingError("A valid message retry key is required",422);const lookup=()=>db.selectFrom("chat_messages").selectAll().where("conversation_id","=",id).where("sender_id","=",actorId).where("client_key","=",clientKey).executeTakeFirst();let row=await lookup();if(!row){try{await db.insertInto("chat_messages").values({conversation_id:id,sender_id:actorId,body:body.trim(),client_key:clientKey,created_at:sqlDateTime(),read_at:null}).execute()}catch(error){if(!duplicate(error))throw error}row=await lookup()}if(!row)throw Error("Message could not be saved");if(row.body!==body.trim())throw new MessagingError("This retry key belongs to a different message",409);return message(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);await db.updateTable("chat_messages").set({read_at:sqlDateTime()}).where("conversation_id","=",id).where("sender_id","=",recipientId).where("id","<=",throughId).whereNull("read_at").execute()}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{db,parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";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)}`}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 message(row){return{id:Number(row.id),conversation_id:String(row.conversation_id),sender_id:Number(row.sender_id),body:String(row.body),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),rows=[...await query().where("first_id","=",actorId).execute(),...await query().where("second_id","=",actorId).execute()],results=[];for(const row of rows){const recipientId=Number(row.first_id)===actorId?Number(row.second_id):Number(row.first_id);if(!await options.authorize(actorId,recipientId))continue;const latest=await db.selectFrom("chat_messages").selectAll().where("conversation_id","=",row.id).orderBy("id","desc").limit(1).executeTakeFirst(),unread=await db.selectFrom("chat_messages").select("id").where("conversation_id","=",row.id).where("sender_id","=",recipientId).whereNull("read_at").count();results.push({id:String(row.id),recipient_id:recipientId,last_message:latest?message(latest):null,unread:Number(unread)})}return results.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){const recipientId=await conversation(id);if(typeof body!=="string"||!body.trim()||body.trim().length>maxLength)throw new MessagingError(`Write a message of 1 to ${maxLength} characters`,422);if(typeof clientKey!=="string"||!/^[\w-]{1,100}$/.test(clientKey))throw new MessagingError("A valid message retry key is required",422);const 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:body.trim(),client_key:clientKey,created_at:sqlDateTime(),read_at:null}).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!==body.trim())throw new MessagingError("This retry key belongs to a different message",409);if(inserted)notify(recipientId,id,"messaging.sent");return message(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,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.121",
5
+ "version": "0.75.122",
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.121",
67
- "@stacksjs/cli": "0.75.121",
68
- "@stacksjs/config": "0.75.121",
69
- "@stacksjs/database": "0.75.121",
70
- "@stacksjs/env": "0.75.121",
71
- "@stacksjs/error-handling": "0.75.121",
72
- "@stacksjs/events": "0.75.121",
73
- "@stacksjs/logging": "0.75.121",
74
- "@stacksjs/model-meta": "0.75.121",
75
- "@stacksjs/pagination": "0.75.121",
76
- "@stacksjs/path": "0.75.121",
77
- "@stacksjs/query-builder": "0.75.121",
78
- "@stacksjs/realtime": "0.75.121",
79
- "@stacksjs/router": "0.75.121",
80
- "@stacksjs/storage": "0.75.121",
81
- "@stacksjs/strings": "0.75.121",
66
+ "@stacksjs/cache": "0.75.122",
67
+ "@stacksjs/cli": "0.75.122",
68
+ "@stacksjs/config": "0.75.122",
69
+ "@stacksjs/database": "0.75.122",
70
+ "@stacksjs/env": "0.75.122",
71
+ "@stacksjs/error-handling": "0.75.122",
72
+ "@stacksjs/events": "0.75.122",
73
+ "@stacksjs/logging": "0.75.122",
74
+ "@stacksjs/model-meta": "0.75.122",
75
+ "@stacksjs/pagination": "0.75.122",
76
+ "@stacksjs/path": "0.75.122",
77
+ "@stacksjs/query-builder": "0.75.122",
78
+ "@stacksjs/realtime": "0.75.122",
79
+ "@stacksjs/router": "0.75.122",
80
+ "@stacksjs/storage": "0.75.122",
81
+ "@stacksjs/strings": "0.75.122",
82
82
  "@stacksjs/ts-validation": "^0.5.6",
83
- "@stacksjs/validation": "0.75.121",
83
+ "@stacksjs/validation": "0.75.122",
84
84
  "bun-query-builder": "^0.3.5"
85
85
  },
86
86
  "peerDependencies": {
87
- "@stacksjs/auth": "0.75.121",
88
- "@stacksjs/payments": "0.75.121",
89
- "@stacksjs/queue": "0.75.121",
90
- "@stacksjs/search-engine": "0.75.121",
87
+ "@stacksjs/auth": "0.75.122",
88
+ "@stacksjs/payments": "0.75.122",
89
+ "@stacksjs/queue": "0.75.122",
90
+ "@stacksjs/search-engine": "0.75.122",
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.121",
111
+ "@stacksjs/types": "0.75.122",
112
112
  "better-dx": "^0.2.24",
113
113
  "stripe": "23.0.0"
114
114
  }