@stacksjs/newsletter 0.72.11 → 0.72.12

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
@@ -2,6 +2,7 @@ export * from './campaigns';
2
2
  export * from './delivery';
3
3
  export * from './headers';
4
4
  export * from './lists';
5
+ export * from './models';
5
6
  export * from './newsletter';
6
7
  export * from './subscriptions';
7
8
  export * from './sync';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- var o=import.meta.require;import{db as R,sqlDateTime as _}from"@stacksjs/database";function E(f){return f.toLowerCase().trim().replace(/[^\w\s-]+/g,"").replace(/\s+/g,"-").replace(/-+/g,"-")}function L(f){return{name:f.name,slug:f.slug??E(f.name),description:f.description??null,status:"active",is_public:f.isPublic===!1?0:1,double_opt_in:f.doubleOptIn===!1?0:1,subscriber_count:0,active_count:0,unsubscribed_count:0,bounced_count:0}}var Q={async create(f){let{EmailList:U}=await import("@stacksjs/orm");return U.create(L(f))},async find(f){let{EmailList:U}=await import("@stacksjs/orm");if(typeof f==="number")return U.find(f);return U.where("slug",f).first()},async all(){let{EmailList:f}=await import("@stacksjs/orm");return f.where("status","active").get()},async archive(f){let U=await Q.find(f);if(!U)throw Error(`[newsletter] List '${String(f)}' not found`);return U.update({status:"archived"})}};async function V(f){if(f.emailListId)return f.emailListId;if(f.emailListSlug){let U=await Q.find(f.emailListSlug);if(!U)throw Error(`[newsletter] List '${f.emailListSlug}' not found`);return U.id}throw Error("[newsletter] Campaign requires emailListId or emailListSlug")}class C extends Error{constructor(f){super(`[newsletter] Campaign ${f} changed before delivery could be queued`);this.name="CampaignStateConflictError"}}function W(f,U,w){if(typeof f?.get==="function"){let x=f.get(U);if(x!==void 0)return x;return f.get(w)}return f?.[U]??f?.[w]}function X(f){let U=W(f,"scheduled_at","scheduledAt"),w=W(f,"updated_at","updatedAt");return{status:String(W(f,"status","status")||""),scheduledAt:U===null||U===void 0||U===""?null:String(U),updatedAt:w===null||w===void 0||w===""?null:String(w)}}function b(f,U,w,x){return`newsletter:campaign:${f}:${U}:${x||"now"}:${w}`}function M(f){return["draft","scheduled","paused","failed"].includes(String(f||"").toLowerCase())}function q(f){if(Array.isArray(f))return f.reduce((x,n)=>x+q(n),0);if(typeof f==="number"||typeof f==="bigint")return Number(f);let U=f,w=U?.changes??U?.affectedRows??U?.numAffectedRows??U?.numUpdatedRows;if(typeof w==="object"&&w!==null)return Number(w.changes||0);return Number(w||0)}function K(f,U){return U===null?f.whereNull("scheduled_at"):f.where("scheduled_at","=",U)}function S(f,U){return U===null?f.whereNull("updated_at"):f.where("updated_at","=",U)}async function Y(f,U,w){let n=await S(K(R.updateTable("campaigns").set({status:w.status,scheduled_at:w.scheduledAt,updated_at:w.updatedAt}).where("id","=",f).where("status","=",U.status),U.scheduledAt),U.updatedAt).execute();if(q(n)!==1)throw new C(f)}async function k(f,U,w){await K(R.updateTable("campaigns").set({status:w.status,scheduled_at:w.scheduledAt,updated_at:_()}).where("id","=",f).where("status","=",U.status).where("updated_at","=",U.updatedAt),U.scheduledAt).execute()}async function D(f,U,w,x,n,F=0){let{job:N}=await import("@stacksjs/queue"),r=n?"scheduled":"immediate";try{await N("SendCampaign",{campaignId:f,chunkSize:x.chunkSize??50,dryRun:x.dryRun??!1,...n?{scheduledAt:n}:{}}).onQueue("campaigns").delay(F).withIdempotencyKey(b(f,r,U.attemptId,n)).dispatch()}catch(P){try{await k(f,U,w)}catch($){throw AggregateError([P,$],`[newsletter] Campaign ${f} could not be dispatched or restored`)}throw P}}function a(f,U){if(!U)return!0;if(String(f.status||"")!=="scheduled")return!1;let w=f.scheduled_at??f.scheduledAt,x=new Date(String(w||"")).getTime(),n=new Date(U).getTime();return Number.isFinite(x)&&Number.isFinite(n)&&x===n}function A(f,U){return{name:f.name,description:f.description??null,type:"email",status:f.scheduledAt?"scheduled":"draft",subject:f.subject,template:f.template,text:f.text??null,from_name:f.fromName??null,from_address:f.fromAddress??null,email_list_id:U,scheduled_at:f.scheduledAt??null,sent_count:0}}function g(f,U){let w={};if(f.name!==void 0)w.name=f.name;if(f.description!==void 0)w.description=f.description;if(f.subject!==void 0)w.subject=f.subject;if(f.template!==void 0)w.template=f.template;if(f.text!==void 0)w.text=f.text;if(f.fromName!==void 0)w.from_name=f.fromName;if(f.fromAddress!==void 0)w.from_address=f.fromAddress;if(U!==void 0)w.email_list_id=U;if(f.scheduledAt!==void 0)w.scheduled_at=f.scheduledAt||null,w.status=f.scheduledAt?"scheduled":"draft";return w}var H={async create(f){let{Campaign:U}=await import("@stacksjs/orm"),w=await V(f);return U.create(A(f,w))},async find(f){let{Campaign:U}=await import("@stacksjs/orm");return U.find(f)},async update(f,U){let w=await H.find(f);if(!w)throw Error(`[newsletter] Campaign ${f} not found`);if(w.status!=="draft"&&w.status!=="scheduled")throw Error(`[newsletter] Cannot edit campaign in status '${w.status}'`);let n=U.emailListId!==void 0||U.emailListSlug!==void 0?await V(U):void 0;return w.update(g(U,n))},async sendNow(f,U={}){let w=await H.find(f);if(!w)throw Error(`[newsletter] Campaign ${f} not found`);if(!M(w.status))throw Error(`[newsletter] Campaign ${f} in status '${w.status}' cannot be queued`);let x=X(w),n={status:"sending",scheduledAt:x.scheduledAt,updatedAt:_(),attemptId:crypto.randomUUID()};return await Y(f,x,n),await D(f,n,x,U),{ok:!0,campaignId:f}},async schedule(f,U,w={}){let x=await H.find(f);if(!x)throw Error(`[newsletter] Campaign ${f} not found`);if(!M(x.status))throw Error(`[newsletter] Campaign ${f} in status '${x.status}' cannot be scheduled`);let n=U instanceof Date?U:new Date(U);if(!Number.isFinite(n.getTime()))throw Error("[newsletter] scheduledAt must be a valid date");let F=Math.max(0,Math.floor((n.getTime()-Date.now())/1000)),N=n.toISOString(),r=X(x),P={status:F===0?"sending":"scheduled",scheduledAt:N,updatedAt:_(),attemptId:crypto.randomUUID()};return await Y(f,r,P),await D(f,P,r,w,F>0?N:void 0,F),{ok:!0,campaignId:f,scheduledAt:N}},async cancel(f){let U=await H.find(f);if(!U)throw Error(`[newsletter] Campaign ${f} not found`);if(U.status==="sent")throw Error(`[newsletter] Campaign ${f} already sent. It cannot be cancelled`);let w=X(U),x={status:"cancelled",scheduledAt:w.scheduledAt,updatedAt:_(),attemptId:crypto.randomUUID()};return await Y(f,w,x),H.find(f)}};function m(f){let U=2166136261;for(let w=0;w<f.length;w++)U^=f.charCodeAt(w),U=Math.imul(U,16777619);return U>>>0}function Uf(f,U){if(U.length===0)throw Error("At least one delivery variant is required");let w=U.reduce((F,N)=>F+N.allocation,0);if(w<=0)throw Error("Delivery variant allocation must be greater than zero");let x=m(f)%w,n=0;for(let F of U){if(!Number.isFinite(F.allocation)||F.allocation<0)throw Error("Delivery variant allocation must be a non-negative number");if(n+=F.allocation,x<n)return F}return U[U.length-1]}function wf(f){return["delivery",f.teamId,f.campaignId,f.channel,f.recipient.trim().toLowerCase(),f.occurrence||"once"].join(":")}function xf(f){let U=f.find((w)=>{if(w.limit===null)return!1;return w.used+(w.requested??1)>w.limit});if(U)throw Error(`Usage limit exceeded for ${U.meter}`)}function nf(f,U=""){let w=U.toLowerCase();if(["suppressed","unsubscribe","complaint","stop"].some((x)=>w.includes(x)))return"suppressed";if(Number(f)===429||w.includes("rate"))return"rate_limited";if(Number(f)>=500||["timeout","temporary","unavailable"].some((x)=>w.includes(x)))return"transient";return"permanent"}function Nf(f){if(!f.url||!/^https?:\/\//i.test(f.url))throw Error("[newsletter] List-Unsubscribe URL must be absolute (http/https)");let U=[];if(f.mailto)U.push(`<mailto:${f.mailto}>`);return U.push(`<${f.url}>`),{"List-Unsubscribe":U.join(", "),"List-Unsubscribe-Post":"List-Unsubscribe=One-Click"}}async function h(f){if(f==null){let w=await Q.find("default");if(!w)w=await Q.create({name:"Default",slug:"default"});return w.id}let U=await Q.find(f);if(!U)throw Error(`[newsletter] List '${String(f)}' not found`);return U.id}async function G(f,U={}){if(!f||!f.includes("@"))throw Error("[newsletter] subscribe() requires a valid email address");let{Subscriber:w,EmailListSubscriber:x}=await import("@stacksjs/orm"),n=await h(U.list),F=U.source??"api",N=await w.where("email",f).first();if(!N)N=await w.create({email:f,status:"subscribed",source:F});let r=await x.where("subscriber_id",N.id).where("email_list_id",n).first();if(r){if(r.status==="unsubscribed")await r.update({status:"subscribed",unsubscribed_at:null});return{created:!1,email:f,listId:n,token:r.uuid}}let P=await x.create({subscriber_id:N.id,email_list_id:n,status:"subscribed",source:F});return{created:!0,email:f,listId:n,token:P.uuid}}async function T(f){if(!f)return{ok:!1};let{EmailListSubscriber:U,Subscriber:w}=await import("@stacksjs/orm"),x=await U.where("uuid",f).first();if(!x)return{ok:!1};if(x.status==="unsubscribed")return{ok:!0,alreadyUnsubscribed:!0,email:(await w.find(x.subscriber_id))?.email,listId:x.email_list_id};return await x.update({status:"unsubscribed",unsubscribed_at:new Date().toISOString()}),{ok:!0,email:(await w.find(x.subscriber_id))?.email,listId:x.email_list_id}}async function O(f){let{Subscriber:U,EmailListSubscriber:w}=await import("@stacksjs/orm"),x=await U.where("email",f).first();if(!x)return 0;let n=await w.where("subscriber_id",x.id).where("status","subscribed").get();for(let F of n)await F.update({status:"unsubscribed",unsubscribed_at:new Date().toISOString()});return n.length}var Hf={lists:Q,campaigns:H,subscribe:G,unsubscribe:T,unsubscribeAll:O};function J(f){return Boolean(f)&&f.includes("@")&&!f.startsWith("@")&&!f.endsWith("@")}function u(f,U){if(!J(f))return"skip-invalid";if(U.duplicate)return"skip-duplicate";if(U.suppressed)return"skip-suppressed";if(U.listStatus==="unsubscribed")return"skip-unsubscribed";return U.listStatus==="subscribed"?"existing":"add"}async function Jf(f,U,w={}){let{EmailListSubscriber:x,Subscriber:n}=await import("@stacksjs/orm"),F={added:0,existing:0,unsubscribed:0,suppressed:0,invalid:0,skipped:{unsubscribed:[],suppressed:[],invalid:[]}},N=new Set;for(let r of U){let P=String(r.email??""),$=P.trim().toLowerCase(),Z=null;if(J($)&&!N.has($)){let B=await n.where("email",$).first();if(B)Z=(await x.where("subscriber_id",B.id).where("email_list_id",await e(f)).first())?.status??null}let y=u($,{duplicate:N.has($),suppressed:J($)?await l($):!1,listStatus:Z});if(J($))N.add($);switch(y){case"skip-invalid":F.invalid++,F.skipped.invalid.push(P);continue;case"skip-duplicate":continue;case"skip-suppressed":F.suppressed++,F.skipped.suppressed.push($);continue;case"skip-unsubscribed":F.unsubscribed++,F.skipped.unsubscribed.push($);continue;case"existing":F.existing++;continue;case"add":break}if((await G($,{list:f,source:r.source??w.source??"sync",upsert:!0})).created)F.added++;else F.existing++}return F}async function l(f){try{let{isSuppressed:U}=await import("@stacksjs/email");return await U(f)}catch{return!1}}async function e(f){if(typeof f==="number")return f;let{EmailList:U}=await import("@stacksjs/orm"),w=await U.where("slug",f).first();return Number(w?.id??0)}class z extends Error{code;constructor(f,U){let w={unavailable:`Usage meter unavailable for ${U}`,invalid:`Usage meter state is invalid for ${U}`,exceeded:`Usage limit exceeded for ${U}`,contention:`Usage reservation contention for ${U}`};super(w[f]);this.code=f;this.name="UsageQuotaError"}}function v(f){return Number.isFinite(f)&&f>0}function j(f,U){return f.meter===U&&Number.isFinite(f.used)&&f.used>=0&&(f.limit===null||Number.isFinite(f.limit)&&f.limit>=0)}function I(f){return Number.isInteger(f)&&f>0?f:1}async function Xf(f,U,w=1,x=5){if(!U||!v(w))throw new z("invalid",U||"unknown");for(let n=0;n<I(x);n++){let F=await f.read(U);if(!F)throw new z("unavailable",U);if(!j(F,U))throw new z("invalid",U);let N=F.used+w;if(F.limit!==null&&N>F.limit)throw new z("exceeded",U);if(await f.compareAndSet(F,N))return{meter:U,quantity:w,previousUsed:F.used,used:N,limit:F.limit}}throw new z("contention",U)}async function Yf(f,U,w=5){if(!U.meter||!v(U.quantity))throw new z("invalid",U.meter||"unknown");for(let x=0;x<I(w);x++){let n=await f.read(U.meter);if(!n)throw new z("unavailable",U.meter);if(!j(n,U.meter)||n.used<U.quantity)throw new z("invalid",U.meter);if(await f.compareAndSet(n,n.used-U.quantity))return}throw new z("contention",U.meter)}export{O as unsubscribeAll,T as unsubscribe,Jf as syncListSubscribers,u as syncDecision,G as subscribe,a as shouldRunScheduledCampaign,Xf as reserveUsage,Yf as releaseUsage,Hf as newsletter,J as looksLikeEmail,Q as lists,L as emailListCreateData,wf as deliveryIdempotencyKey,nf as classifyDeliveryFailure,M as canQueueCampaignStatus,H as campaigns,g as campaignUpdateData,X as campaignDeliverySnapshot,b as campaignDeliveryDispatchKey,A as campaignCreateData,Nf as buildUnsubscribeHeaders,xf as assertUsageAvailable,Uf as allocateDeliveryVariant,z as UsageQuotaError,C as CampaignStateConflictError};
2
+ var Z=import.meta.require;function C(f){return Boolean(f)&&typeof f?.where==="function"}async function P(f){let U=globalThis[f];if(C(U))return U;let w=(await import("@stacksjs/orm"))[f];if(C(w))return w;throw Error(`[newsletter] Model '${f}' is not available. Its feature's models may not be installed in this app - try \`buddy marketing:install\`.`)}import{db as K,sqlDateTime as W}from"@stacksjs/database";function k(f){return f.toLowerCase().trim().replace(/[^\w\s-]+/g,"").replace(/\s+/g,"-").replace(/-+/g,"-")}function S(f){return{name:f.name,slug:f.slug??k(f.name),description:f.description??null,status:"active",is_public:f.isPublic===!1?0:1,double_opt_in:f.doubleOptIn===!1?0:1,subscriber_count:0,active_count:0,unsubscribed_count:0,bounced_count:0}}var J={async create(f){return(await P("EmailList")).create(S(f))},async find(f){let U=await P("EmailList");if(typeof f==="number")return U.find(f);return U.where("slug",f).first()},async all(){return(await P("EmailList")).where("status","active").get()},async archive(f){let U=await J.find(f);if(!U)throw Error(`[newsletter] List '${String(f)}' not found`);return U.update({status:"archived"})}};async function D(f){if(f.emailListId)return f.emailListId;if(f.emailListSlug){let U=await J.find(f.emailListSlug);if(!U)throw Error(`[newsletter] List '${f.emailListSlug}' not found`);return U.id}throw Error("[newsletter] Campaign requires emailListId or emailListSlug")}class O extends Error{constructor(f){super(`[newsletter] Campaign ${f} changed before delivery could be queued`);this.name="CampaignStateConflictError"}}function r(f,U,x){if(typeof f?.get==="function"){let w=f.get(U);if(w!==void 0)return w;return f.get(x)}return f?.[U]??f?.[x]}function B(f){let U=r(f,"scheduled_at","scheduledAt"),x=r(f,"updated_at","updatedAt");return{status:String(r(f,"status","status")||""),scheduledAt:U===null||U===void 0||U===""?null:String(U),updatedAt:x===null||x===void 0||x===""?null:String(x)}}function b(f,U,x,w){return`newsletter:campaign:${f}:${U}:${w||"now"}:${x}`}function R(f){return["draft","scheduled","paused","failed"].includes(String(f||"").toLowerCase())}function T(f){if(Array.isArray(f))return f.reduce((w,F)=>w+T(F),0);if(typeof f==="number"||typeof f==="bigint")return Number(f);let U=f,x=U?.changes??U?.affectedRows??U?.numAffectedRows??U?.numUpdatedRows;if(typeof x==="object"&&x!==null)return Number(x.changes||0);return Number(x||0)}function v(f,U){return U===null?f.whereNull("scheduled_at"):f.where("scheduled_at","=",U)}function A(f,U){return U===null?f.whereNull("updated_at"):f.where("updated_at","=",U)}async function V(f,U,x){let F=await A(v(K.updateTable("campaigns").set({status:x.status,scheduled_at:x.scheduledAt,updated_at:x.updatedAt}).where("id","=",f).where("status","=",U.status),U.scheduledAt),U.updatedAt).execute();if(T(F)!==1)throw new O(f)}async function g(f,U,x){await v(K.updateTable("campaigns").set({status:x.status,scheduled_at:x.scheduledAt,updated_at:W()}).where("id","=",f).where("status","=",U.status).where("updated_at","=",U.updatedAt),U.scheduledAt).execute()}async function q(f,U,x,w,F,N=0){let{job:$}=await import("@stacksjs/queue"),H=F?"scheduled":"immediate";try{await $("SendCampaign",{campaignId:f,chunkSize:w.chunkSize??50,dryRun:w.dryRun??!1,...F?{scheduledAt:F}:{}}).onQueue("campaigns").delay(N).withIdempotencyKey(b(f,H,U.attemptId,F)).dispatch()}catch(_){try{await g(f,U,x)}catch(z){throw AggregateError([_,z],`[newsletter] Campaign ${f} could not be dispatched or restored`)}throw _}}function Ff(f,U){if(!U)return!0;if(String(f.status||"")!=="scheduled")return!1;let x=f.scheduled_at??f.scheduledAt,w=new Date(String(x||"")).getTime(),F=new Date(U).getTime();return Number.isFinite(w)&&Number.isFinite(F)&&w===F}function h(f,U){return{name:f.name,description:f.description??null,type:"email",status:f.scheduledAt?"scheduled":"draft",subject:f.subject,template:f.template,text:f.text??null,from_name:f.fromName??null,from_address:f.fromAddress??null,email_list_id:U,scheduled_at:f.scheduledAt??null,sent_count:0}}function m(f,U){let x={};if(f.name!==void 0)x.name=f.name;if(f.description!==void 0)x.description=f.description;if(f.subject!==void 0)x.subject=f.subject;if(f.template!==void 0)x.template=f.template;if(f.text!==void 0)x.text=f.text;if(f.fromName!==void 0)x.from_name=f.fromName;if(f.fromAddress!==void 0)x.from_address=f.fromAddress;if(U!==void 0)x.email_list_id=U;if(f.scheduledAt!==void 0)x.scheduled_at=f.scheduledAt||null,x.status=f.scheduledAt?"scheduled":"draft";return x}var Q={async create(f){let U=await P("Campaign"),x=await D(f);return U.create(h(f,x))},async find(f){return(await P("Campaign")).find(f)},async update(f,U){let x=await Q.find(f);if(!x)throw Error(`[newsletter] Campaign ${f} not found`);if(x.status!=="draft"&&x.status!=="scheduled")throw Error(`[newsletter] Cannot edit campaign in status '${x.status}'`);let F=U.emailListId!==void 0||U.emailListSlug!==void 0?await D(U):void 0;return x.update(m(U,F))},async sendNow(f,U={}){let x=await Q.find(f);if(!x)throw Error(`[newsletter] Campaign ${f} not found`);if(!R(x.status))throw Error(`[newsletter] Campaign ${f} in status '${x.status}' cannot be queued`);let w=B(x),F={status:"sending",scheduledAt:w.scheduledAt,updatedAt:W(),attemptId:crypto.randomUUID()};return await V(f,w,F),await q(f,F,w,U),{ok:!0,campaignId:f}},async schedule(f,U,x={}){let w=await Q.find(f);if(!w)throw Error(`[newsletter] Campaign ${f} not found`);if(!R(w.status))throw Error(`[newsletter] Campaign ${f} in status '${w.status}' cannot be scheduled`);let F=U instanceof Date?U:new Date(U);if(!Number.isFinite(F.getTime()))throw Error("[newsletter] scheduledAt must be a valid date");let N=Math.max(0,Math.floor((F.getTime()-Date.now())/1000)),$=F.toISOString(),H=B(w),_={status:N===0?"sending":"scheduled",scheduledAt:$,updatedAt:W(),attemptId:crypto.randomUUID()};return await V(f,H,_),await q(f,_,H,x,N>0?$:void 0,N),{ok:!0,campaignId:f,scheduledAt:$}},async cancel(f){let U=await Q.find(f);if(!U)throw Error(`[newsletter] Campaign ${f} not found`);if(U.status==="sent")throw Error(`[newsletter] Campaign ${f} already sent. It cannot be cancelled`);let x=B(U),w={status:"cancelled",scheduledAt:x.scheduledAt,updatedAt:W(),attemptId:crypto.randomUUID()};return await V(f,x,w),Q.find(f)}};function u(f){let U=2166136261;for(let x=0;x<f.length;x++)U^=f.charCodeAt(x),U=Math.imul(U,16777619);return U>>>0}function $f(f,U){if(U.length===0)throw Error("At least one delivery variant is required");let x=U.reduce((N,$)=>N+$.allocation,0);if(x<=0)throw Error("Delivery variant allocation must be greater than zero");let w=u(f)%x,F=0;for(let N of U){if(!Number.isFinite(N.allocation)||N.allocation<0)throw Error("Delivery variant allocation must be a non-negative number");if(F+=N.allocation,w<F)return N}return U[U.length-1]}function Pf(f){return["delivery",f.teamId,f.campaignId,f.channel,f.recipient.trim().toLowerCase(),f.occurrence||"once"].join(":")}function zf(f){let U=f.find((x)=>{if(x.limit===null)return!1;return x.used+(x.requested??1)>x.limit});if(U)throw Error(`Usage limit exceeded for ${U.meter}`)}function Hf(f,U=""){let x=U.toLowerCase();if(["suppressed","unsubscribe","complaint","stop"].some((w)=>x.includes(w)))return"suppressed";if(Number(f)===429||x.includes("rate"))return"rate_limited";if(Number(f)>=500||["timeout","temporary","unavailable"].some((w)=>x.includes(w)))return"transient";return"permanent"}function Gf(f){if(!f.url||!/^https?:\/\//i.test(f.url))throw Error("[newsletter] List-Unsubscribe URL must be absolute (http/https)");let U=[];if(f.mailto)U.push(`<mailto:${f.mailto}>`);return U.push(`<${f.url}>`),{"List-Unsubscribe":U.join(", "),"List-Unsubscribe-Post":"List-Unsubscribe=One-Click"}}async function l(f){if(f==null){let x=await J.find("default");if(!x)x=await J.create({name:"Default",slug:"default"});return x.id}let U=await J.find(f);if(!U)throw Error(`[newsletter] List '${String(f)}' not found`);return U.id}async function X(f,U={}){if(!f||!f.includes("@"))throw Error("[newsletter] subscribe() requires a valid email address");let[x,w]=await Promise.all([P("Subscriber"),P("EmailListSubscriber")]),F=await l(U.list),N=U.source??"api",$=await x.where("email",f).first();if(!$)$=await x.create({email:f,status:"subscribed",source:N});let H=await w.where("subscriber_id",$.id).where("email_list_id",F).first();if(H){if(H.status==="unsubscribed")await H.update({status:"subscribed",unsubscribed_at:null});return{created:!1,email:f,listId:F,token:H.uuid}}let _=await w.create({subscriber_id:$.id,email_list_id:F,status:"subscribed",source:N});return{created:!0,email:f,listId:F,token:_.uuid}}async function I(f){if(!f)return{ok:!1};let[U,x]=await Promise.all([P("EmailListSubscriber"),P("Subscriber")]),w=await U.where("uuid",f).first();if(!w)return{ok:!1};if(w.status==="unsubscribed")return{ok:!0,alreadyUnsubscribed:!0,email:(await x.find(w.subscriber_id))?.email,listId:w.email_list_id};return await w.update({status:"unsubscribed",unsubscribed_at:new Date().toISOString()}),{ok:!0,email:(await x.find(w.subscriber_id))?.email,listId:w.email_list_id}}async function E(f){let[U,x]=await Promise.all([P("Subscriber"),P("EmailListSubscriber")]),w=await U.where("email",f).first();if(!w)return 0;let F=await x.where("subscriber_id",w.id).where("status","subscribed").get();for(let N of F)await N.update({status:"unsubscribed",unsubscribed_at:new Date().toISOString()});return F.length}var Bf={lists:J,campaigns:Q,subscribe:X,unsubscribe:I,unsubscribeAll:E};function Y(f){return Boolean(f)&&f.includes("@")&&!f.startsWith("@")&&!f.endsWith("@")}function c(f,U){if(!Y(f))return"skip-invalid";if(U.duplicate)return"skip-duplicate";if(U.suppressed)return"skip-suppressed";if(U.listStatus==="unsubscribed")return"skip-unsubscribed";return U.listStatus==="subscribed"?"existing":"add"}async function Cf(f,U,x={}){let[w,F]=await Promise.all([P("EmailListSubscriber"),P("Subscriber")]),N={added:0,existing:0,unsubscribed:0,suppressed:0,invalid:0,skipped:{unsubscribed:[],suppressed:[],invalid:[]}},$=new Set;for(let H of U){let _=String(H.email??""),z=_.trim().toLowerCase(),M=null;if(Y(z)&&!$.has(z)){let j=await F.where("email",z).first();if(j)M=(await w.where("subscriber_id",j.id).where("email_list_id",await e(f)).first())?.status??null}let L=c(z,{duplicate:$.has(z),suppressed:Y(z)?await d(z):!1,listStatus:M});if(Y(z))$.add(z);switch(L){case"skip-invalid":N.invalid++,N.skipped.invalid.push(_);continue;case"skip-duplicate":continue;case"skip-suppressed":N.suppressed++,N.skipped.suppressed.push(z);continue;case"skip-unsubscribed":N.unsubscribed++,N.skipped.unsubscribed.push(z);continue;case"existing":N.existing++;continue;case"add":break}if((await X(z,{list:f,source:H.source??x.source??"sync",upsert:!0})).created)N.added++;else N.existing++}return N}async function d(f){try{let{isSuppressed:U}=await import("@stacksjs/email");return await U(f)}catch{return!1}}async function e(f){if(typeof f==="number")return f;let x=await(await P("EmailList")).where("slug",f).first();return Number(x?.id??0)}class G extends Error{code;constructor(f,U){let x={unavailable:`Usage meter unavailable for ${U}`,invalid:`Usage meter state is invalid for ${U}`,exceeded:`Usage limit exceeded for ${U}`,contention:`Usage reservation contention for ${U}`};super(x[f]);this.code=f;this.name="UsageQuotaError"}}function n(f){return Number.isFinite(f)&&f>0}function o(f,U){return f.meter===U&&Number.isFinite(f.used)&&f.used>=0&&(f.limit===null||Number.isFinite(f.limit)&&f.limit>=0)}function y(f){return Number.isInteger(f)&&f>0?f:1}async function Rf(f,U,x=1,w=5){if(!U||!n(x))throw new G("invalid",U||"unknown");for(let F=0;F<y(w);F++){let N=await f.read(U);if(!N)throw new G("unavailable",U);if(!o(N,U))throw new G("invalid",U);let $=N.used+x;if(N.limit!==null&&$>N.limit)throw new G("exceeded",U);if(await f.compareAndSet(N,$))return{meter:U,quantity:x,previousUsed:N.used,used:$,limit:N.limit}}throw new G("contention",U)}async function qf(f,U,x=5){if(!U.meter||!n(U.quantity))throw new G("invalid",U.meter||"unknown");for(let w=0;w<y(x);w++){let F=await f.read(U.meter);if(!F)throw new G("unavailable",U.meter);if(!o(F,U.meter)||F.used<U.quantity)throw new G("invalid",U.meter);if(await f.compareAndSet(F,F.used-U.quantity))return}throw new G("contention",U.meter)}export{E as unsubscribeAll,I as unsubscribe,Cf as syncListSubscribers,c as syncDecision,X as subscribe,Ff as shouldRunScheduledCampaign,Rf as reserveUsage,qf as releaseUsage,Bf as newsletter,P as model,Y as looksLikeEmail,J as lists,S as emailListCreateData,Pf as deliveryIdempotencyKey,Hf as classifyDeliveryFailure,R as canQueueCampaignStatus,Q as campaigns,m as campaignUpdateData,B as campaignDeliverySnapshot,b as campaignDeliveryDispatchKey,h as campaignCreateData,Gf as buildUnsubscribeHeaders,zf as assertUsageAvailable,$f as allocateDeliveryVariant,G as UsageQuotaError,O as CampaignStateConflictError};
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The live model called `name`.
3
+ *
4
+ * Throws with the model's name rather than letting `undefined.where` surface
5
+ * three frames later: a missing model almost always means the feature's
6
+ * models were never published into the app (`buddy marketing:install`), and
7
+ * the message should say which one is missing.
8
+ */
9
+ export declare function model(name: string): Promise<QueryableModel>;
10
+ /**
11
+ * Getting at the models, in both the places this package runs.
12
+ *
13
+ * Every function here used to open with
14
+ * `const { EmailList } = await import('@stacksjs/orm')`. That works inside the
15
+ * framework repo, where the ORM package exports generated models complete with
16
+ * their query statics.
17
+ *
18
+ * It does NOT work in an installed app. There, the live models are the
19
+ * auto-import globals the preloader wires up, and the package's named export
20
+ * is a bare definition with no `.where()` on it - so every call in this file
21
+ * failed with "EmailList.where is not a function", and the whole package was
22
+ * unusable in exactly the apps it exists for.
23
+ *
24
+ * `model()` asks for the live one first and falls back to the package export,
25
+ * so the same code works in the framework, in an app, and in a test.
26
+ */
27
+ export declare interface QueryableModel {
28
+ find: (id: number) => Promise<any>
29
+ where: (...args: any[]) => any
30
+ create: (data: Record<string, unknown>) => Promise<any>
31
+ [key: string]: any
32
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/newsletter",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.11",
5
+ "version": "0.72.12",
6
6
  "description": "Promotional email & newsletter primitives for Stacks. Lists, campaigns, subscribers, and one-click unsubscribe — sent through @stacksjs/email.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -61,15 +61,15 @@
61
61
  "prepublishOnly": "bun run build"
62
62
  },
63
63
  "dependencies": {
64
- "@stacksjs/database": "0.72.11",
65
- "@stacksjs/orm": "0.72.11",
66
- "@stacksjs/queue": "0.72.11"
64
+ "@stacksjs/database": "0.72.12",
65
+ "@stacksjs/orm": "0.72.12",
66
+ "@stacksjs/queue": "0.72.12"
67
67
  },
68
68
  "devDependencies": {
69
- "@stacksjs/cli": "0.72.11",
70
- "@stacksjs/config": "0.72.11",
69
+ "@stacksjs/cli": "0.72.12",
70
+ "@stacksjs/config": "0.72.12",
71
71
  "better-dx": "^0.2.23",
72
- "@stacksjs/error-handling": "0.72.11",
73
- "@stacksjs/types": "0.72.11"
72
+ "@stacksjs/error-handling": "0.72.12",
73
+ "@stacksjs/types": "0.72.12"
74
74
  }
75
75
  }