@stacksjs/orm 0.75.126 → 0.75.127

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.
@@ -13,6 +13,7 @@ export declare interface MessagingOptions {
13
13
  maxLength?: number
14
14
  pageSize?: number
15
15
  broadcast?: boolean
16
+ onMessageSent?: (event: { message: DirectMessage, actorId: number, recipientId: number, scope: string, participantType: string }) => void | Promise<void>
16
17
  authorize: (actorId: number, recipientId: number) => boolean | Promise<boolean>
17
18
  }
18
19
  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";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})}}
1
+ import{createHash}from"node:crypto";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)}`}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);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})}}
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.126",
5
+ "version": "0.75.127",
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.126",
67
- "@stacksjs/cli": "0.75.126",
68
- "@stacksjs/config": "0.75.126",
69
- "@stacksjs/database": "0.75.126",
70
- "@stacksjs/env": "0.75.126",
71
- "@stacksjs/error-handling": "0.75.126",
72
- "@stacksjs/events": "0.75.126",
73
- "@stacksjs/logging": "0.75.126",
74
- "@stacksjs/model-meta": "0.75.126",
75
- "@stacksjs/pagination": "0.75.126",
76
- "@stacksjs/path": "0.75.126",
77
- "@stacksjs/query-builder": "0.75.126",
78
- "@stacksjs/realtime": "0.75.126",
79
- "@stacksjs/router": "0.75.126",
80
- "@stacksjs/storage": "0.75.126",
81
- "@stacksjs/strings": "0.75.126",
66
+ "@stacksjs/cache": "0.75.127",
67
+ "@stacksjs/cli": "0.75.127",
68
+ "@stacksjs/config": "0.75.127",
69
+ "@stacksjs/database": "0.75.127",
70
+ "@stacksjs/env": "0.75.127",
71
+ "@stacksjs/error-handling": "0.75.127",
72
+ "@stacksjs/events": "0.75.127",
73
+ "@stacksjs/logging": "0.75.127",
74
+ "@stacksjs/model-meta": "0.75.127",
75
+ "@stacksjs/pagination": "0.75.127",
76
+ "@stacksjs/path": "0.75.127",
77
+ "@stacksjs/query-builder": "0.75.127",
78
+ "@stacksjs/realtime": "0.75.127",
79
+ "@stacksjs/router": "0.75.127",
80
+ "@stacksjs/storage": "0.75.127",
81
+ "@stacksjs/strings": "0.75.127",
82
82
  "@stacksjs/ts-validation": "^0.5.6",
83
- "@stacksjs/validation": "0.75.126",
83
+ "@stacksjs/validation": "0.75.127",
84
84
  "bun-query-builder": "^0.3.5"
85
85
  },
86
86
  "peerDependencies": {
87
- "@stacksjs/auth": "0.75.126",
88
- "@stacksjs/payments": "0.75.126",
89
- "@stacksjs/queue": "0.75.126",
90
- "@stacksjs/search-engine": "0.75.126",
87
+ "@stacksjs/auth": "0.75.127",
88
+ "@stacksjs/payments": "0.75.127",
89
+ "@stacksjs/queue": "0.75.127",
90
+ "@stacksjs/search-engine": "0.75.127",
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.126",
111
+ "@stacksjs/types": "0.75.127",
112
112
  "better-dx": "^0.2.24",
113
113
  "stripe": "23.0.0"
114
114
  }