@forklaunch/implementation-messaging-twilio 1.0.0
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/LICENSE +21 -0
- package/lib/domain/enum/index.d.ts +3 -0
- package/lib/domain/enum/index.d.ts.map +1 -0
- package/lib/domain/enum/index.js +50 -0
- package/lib/domain/enum/index.mjs +21 -0
- package/lib/domain/enum/twilioMessageStatus.enum.d.ts +16 -0
- package/lib/domain/enum/twilioMessageStatus.enum.d.ts.map +1 -0
- package/lib/domain/schemas/index.d.ts +2 -0
- package/lib/domain/schemas/index.d.ts.map +1 -0
- package/lib/domain/schemas/index.js +86 -0
- package/lib/domain/schemas/index.mjs +75 -0
- package/lib/domain/schemas/sms.schema.d.ts +44 -0
- package/lib/domain/schemas/sms.schema.d.ts.map +1 -0
- package/lib/domain/schemas/typebox/sms.schema.d.ts +39 -0
- package/lib/domain/schemas/typebox/sms.schema.d.ts.map +1 -0
- package/lib/domain/schemas/zod/sms.schema.d.ts +39 -0
- package/lib/domain/schemas/zod/sms.schema.d.ts.map +1 -0
- package/lib/domain/types/index.d.ts +2 -0
- package/lib/domain/types/index.d.ts.map +1 -0
- package/lib/domain/types/index.js +18 -0
- package/lib/domain/types/index.mjs +0 -0
- package/lib/domain/types/twilio.types.d.ts +15 -0
- package/lib/domain/types/twilio.types.d.ts.map +1 -0
- package/lib/eject/domain/enum/index.ts +3 -0
- package/lib/eject/domain/enum/twilioMessageStatus.enum.ts +18 -0
- package/lib/eject/domain/schemas/index.ts +1 -0
- package/lib/eject/domain/schemas/sms.schema.ts +33 -0
- package/lib/eject/domain/types/index.ts +1 -0
- package/lib/eject/domain/types/twilio.types.ts +24 -0
- package/lib/eject/services/index.ts +4 -0
- package/lib/eject/services/sms.service.ts +216 -0
- package/lib/persistence/entities/index.d.ts +92 -0
- package/lib/persistence/entities/index.d.ts.map +1 -0
- package/lib/persistence/entities/index.js +42 -0
- package/lib/persistence/entities/index.mjs +17 -0
- package/lib/services/index.d.ts +4 -0
- package/lib/services/index.d.ts.map +1 -0
- package/lib/services/index.js +193 -0
- package/lib/services/index.mjs +164 -0
- package/lib/services/sms.service.d.ts +38 -0
- package/lib/services/sms.service.d.ts.map +1 -0
- package/package.json +99 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { IdDto } from '@forklaunch/common';
|
|
2
|
+
import {
|
|
3
|
+
evaluateTelemetryOptions,
|
|
4
|
+
MetricsDefinition,
|
|
5
|
+
OpenTelemetryCollector,
|
|
6
|
+
TelemetryOptions
|
|
7
|
+
} from '@forklaunch/core/http';
|
|
8
|
+
import { SmsStatusEnum } from '@forklaunch/implementation-messaging-base/enum';
|
|
9
|
+
import { SmsMappers } from '@forklaunch/implementation-messaging-base/types';
|
|
10
|
+
import { SmsService } from '@forklaunch/interfaces-messaging/interfaces';
|
|
11
|
+
import { SendSmsDto } from '@forklaunch/interfaces-messaging/types';
|
|
12
|
+
import { AnySchemaValidator } from '@forklaunch/validator';
|
|
13
|
+
import { EntityManager, InferEntity } from '@mikro-orm/core';
|
|
14
|
+
import { TwilioMessageStatusEnum } from '../domain/enum/twilioMessageStatus.enum';
|
|
15
|
+
import {
|
|
16
|
+
TwilioMessageResource,
|
|
17
|
+
TwilioSmsDtos,
|
|
18
|
+
TwilioSmsEntities,
|
|
19
|
+
TwilioStatusCallbackDto
|
|
20
|
+
} from '../domain/types/twilio.types';
|
|
21
|
+
import { SmsRecord } from '../persistence/entities';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maps a Twilio message status (from the Messages REST API or a delivery
|
|
25
|
+
* status callback) onto the canonical SmsStatusEnum.
|
|
26
|
+
*/
|
|
27
|
+
export const mapTwilioMessageStatus = (
|
|
28
|
+
messageStatus: string
|
|
29
|
+
): SmsStatusEnum => {
|
|
30
|
+
switch (messageStatus) {
|
|
31
|
+
case TwilioMessageStatusEnum.SENT:
|
|
32
|
+
return SmsStatusEnum.SENT;
|
|
33
|
+
case TwilioMessageStatusEnum.DELIVERED:
|
|
34
|
+
case TwilioMessageStatusEnum.READ:
|
|
35
|
+
return SmsStatusEnum.DELIVERED;
|
|
36
|
+
case TwilioMessageStatusEnum.UNDELIVERED:
|
|
37
|
+
return SmsStatusEnum.UNDELIVERED;
|
|
38
|
+
case TwilioMessageStatusEnum.FAILED:
|
|
39
|
+
case TwilioMessageStatusEnum.CANCELED:
|
|
40
|
+
return SmsStatusEnum.FAILED;
|
|
41
|
+
case TwilioMessageStatusEnum.QUEUED:
|
|
42
|
+
case TwilioMessageStatusEnum.ACCEPTED:
|
|
43
|
+
case TwilioMessageStatusEnum.SCHEDULED:
|
|
44
|
+
case TwilioMessageStatusEnum.SENDING:
|
|
45
|
+
default:
|
|
46
|
+
return SmsStatusEnum.QUEUED;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const TWILIO_API_BASE_URL = 'https://api.twilio.com/2010-04-01';
|
|
51
|
+
|
|
52
|
+
export class TwilioSmsService<
|
|
53
|
+
SchemaValidator extends AnySchemaValidator,
|
|
54
|
+
Entities extends TwilioSmsEntities = TwilioSmsEntities,
|
|
55
|
+
Dto extends TwilioSmsDtos = TwilioSmsDtos
|
|
56
|
+
> implements SmsService<typeof SmsStatusEnum>
|
|
57
|
+
{
|
|
58
|
+
private evaluatedTelemetryOptions: {
|
|
59
|
+
logging?: boolean;
|
|
60
|
+
metrics?: boolean;
|
|
61
|
+
tracing?: boolean;
|
|
62
|
+
};
|
|
63
|
+
protected readonly accountSid: string;
|
|
64
|
+
protected readonly authToken: string;
|
|
65
|
+
protected readonly fromNumber: string;
|
|
66
|
+
public em: EntityManager;
|
|
67
|
+
protected readonly openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>;
|
|
68
|
+
protected readonly schemaValidator: SchemaValidator;
|
|
69
|
+
protected readonly mappers: SmsMappers<typeof SmsStatusEnum, Entities, Dto>;
|
|
70
|
+
|
|
71
|
+
constructor(
|
|
72
|
+
accountSid: string,
|
|
73
|
+
authToken: string,
|
|
74
|
+
fromNumber: string,
|
|
75
|
+
em: EntityManager,
|
|
76
|
+
openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,
|
|
77
|
+
schemaValidator: SchemaValidator,
|
|
78
|
+
mappers: SmsMappers<typeof SmsStatusEnum, Entities, Dto>,
|
|
79
|
+
readonly options?: {
|
|
80
|
+
telemetry?: TelemetryOptions;
|
|
81
|
+
}
|
|
82
|
+
) {
|
|
83
|
+
this.accountSid = accountSid;
|
|
84
|
+
this.authToken = authToken;
|
|
85
|
+
this.fromNumber = fromNumber;
|
|
86
|
+
this.em = em;
|
|
87
|
+
this.openTelemetryCollector = openTelemetryCollector;
|
|
88
|
+
this.schemaValidator = schemaValidator;
|
|
89
|
+
this.mappers = mappers;
|
|
90
|
+
this.evaluatedTelemetryOptions = options?.telemetry
|
|
91
|
+
? evaluateTelemetryOptions(options.telemetry).enabled
|
|
92
|
+
: {
|
|
93
|
+
logging: false,
|
|
94
|
+
metrics: false,
|
|
95
|
+
tracing: false
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async sendSms(
|
|
100
|
+
smsDto: SendSmsDto,
|
|
101
|
+
em?: EntityManager,
|
|
102
|
+
...args: unknown[]
|
|
103
|
+
): Promise<Dto['SmsRecordMapper']> {
|
|
104
|
+
if (this.evaluatedTelemetryOptions.logging) {
|
|
105
|
+
this.openTelemetryCollector.info('Sending sms via twilio', {
|
|
106
|
+
to: smsDto.to
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const smsRecord = await this.mappers.SendSmsMapper.toEntity(
|
|
111
|
+
smsDto,
|
|
112
|
+
em ?? this.em,
|
|
113
|
+
...(args[0] instanceof EntityManager ? args.slice(1) : args)
|
|
114
|
+
);
|
|
115
|
+
const record = smsRecord as {
|
|
116
|
+
status: SmsStatusEnum;
|
|
117
|
+
providerMessageId?: string | null;
|
|
118
|
+
error?: string | null;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// the twilio package is intentionally dependency-free — the message is
|
|
122
|
+
// dispatched via the Twilio Messages REST API with plain fetch
|
|
123
|
+
const response = await fetch(
|
|
124
|
+
`${TWILIO_API_BASE_URL}/Accounts/${this.accountSid}/Messages.json`,
|
|
125
|
+
{
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
Authorization: `Basic ${Buffer.from(
|
|
129
|
+
`${this.accountSid}:${this.authToken}`
|
|
130
|
+
).toString('base64')}`,
|
|
131
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
132
|
+
},
|
|
133
|
+
body: new URLSearchParams({
|
|
134
|
+
To: smsDto.to,
|
|
135
|
+
From: this.fromNumber,
|
|
136
|
+
Body: smsDto.body
|
|
137
|
+
}).toString()
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (response.ok) {
|
|
142
|
+
const message = (await response.json()) as TwilioMessageResource;
|
|
143
|
+
record.providerMessageId = message.sid;
|
|
144
|
+
record.status = mapTwilioMessageStatus(message.status);
|
|
145
|
+
record.error = message.error_message ?? null;
|
|
146
|
+
} else {
|
|
147
|
+
const errorBody = await response.text();
|
|
148
|
+
record.status = SmsStatusEnum.FAILED;
|
|
149
|
+
record.error = `Twilio API error (${response.status}): ${errorBody}`;
|
|
150
|
+
this.openTelemetryCollector.error('Twilio message dispatch failed', {
|
|
151
|
+
status: response.status,
|
|
152
|
+
error: errorBody
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await (em ?? this.em).transactional(async (innerEm) => {
|
|
157
|
+
await innerEm.persist(smsRecord);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return this.mappers.SmsRecordMapper.toDto(
|
|
161
|
+
smsRecord as InferEntity<Entities['SmsRecordMapper']>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async getSmsRecord(
|
|
166
|
+
{ id }: IdDto,
|
|
167
|
+
em?: EntityManager
|
|
168
|
+
): Promise<Dto['SmsRecordMapper']> {
|
|
169
|
+
if (this.evaluatedTelemetryOptions.logging) {
|
|
170
|
+
this.openTelemetryCollector.info('Getting sms record', { id });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const smsRecord = await (em ?? this.em).findOneOrFail(
|
|
174
|
+
this.mappers.SmsRecordMapper.entity as typeof SmsRecord,
|
|
175
|
+
{ id }
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return this.mappers.SmsRecordMapper.toDto(
|
|
179
|
+
smsRecord as InferEntity<Entities['SmsRecordMapper']>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Maps a Twilio delivery-status callback (MessageStatus, MessageSid,
|
|
185
|
+
* ErrorMessage) onto the persisted record's status.
|
|
186
|
+
*/
|
|
187
|
+
async processStatusCallback(
|
|
188
|
+
payload: TwilioStatusCallbackDto,
|
|
189
|
+
em?: EntityManager
|
|
190
|
+
): Promise<Dto['SmsRecordMapper']> {
|
|
191
|
+
if (this.evaluatedTelemetryOptions.logging) {
|
|
192
|
+
this.openTelemetryCollector.info('Processing twilio status callback', {
|
|
193
|
+
messageSid: payload.MessageSid,
|
|
194
|
+
messageStatus: payload.MessageStatus
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const smsRecord = await (em ?? this.em).findOneOrFail(
|
|
199
|
+
this.mappers.SmsRecordMapper.entity as typeof SmsRecord,
|
|
200
|
+
{ providerMessageId: payload.MessageSid }
|
|
201
|
+
);
|
|
202
|
+
const record = smsRecord as {
|
|
203
|
+
status: SmsStatusEnum;
|
|
204
|
+
error?: string | null;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
record.status = mapTwilioMessageStatus(payload.MessageStatus);
|
|
208
|
+
record.error = payload.ErrorMessage ?? null;
|
|
209
|
+
|
|
210
|
+
await (em ?? this.em).flush();
|
|
211
|
+
|
|
212
|
+
return this.mappers.SmsRecordMapper.toDto(
|
|
213
|
+
smsRecord as InferEntity<Entities['SmsRecordMapper']>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export declare const SmsRecord: import("@mikro-orm/core").EntitySchemaWithMeta<"SmsRecord", string, import("@mikro-orm/core").InferEntityFromProperties<{
|
|
2
|
+
readonly id: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "primary"> & {
|
|
3
|
+
primary: true;
|
|
4
|
+
} & {
|
|
5
|
+
readonly '~c': true;
|
|
6
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
7
|
+
readonly to: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, import("@mikro-orm/core").EmptyOptions & {
|
|
8
|
+
readonly '~c': true;
|
|
9
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
10
|
+
readonly body: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, import("@mikro-orm/core").EmptyOptions & {
|
|
11
|
+
readonly '~c': true;
|
|
12
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
13
|
+
readonly status: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<any, import("@mikro-orm/core").EmptyOptions & {
|
|
14
|
+
readonly '~c': true;
|
|
15
|
+
}, "accessor" | "array" | "autoincrement" | "check" | "collation" | "columnType" | "columnTypes" | "comment" | "compliance" | "concurrencyCheck" | "customOrder" | "default" | "defaultRaw" | "extra" | "fieldName" | "fieldNames" | "formula" | "generated" | "getter" | "getterName" | "groups" | "hidden" | "hydrate" | "ignoreSchemaChanges" | "index" | "lazy" | "length" | "name" | "nativeEnumName" | "nullable" | "onCreate" | "onUpdate" | "persist" | "precision" | "primary" | "ref" | "returning" | "runtimeType" | "scale" | "serializedName" | "serializedPrimaryKey" | "serializer" | "setter" | "type" | "unique" | "unsigned" | "version" | ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type")>, "accessor" | "array" | "autoincrement" | "check" | "collation" | "columnType" | "columnTypes" | "comment" | "compliance" | "concurrencyCheck" | "customOrder" | "default" | "defaultRaw" | "extra" | "fieldName" | "fieldNames" | "formula" | "generated" | "getter" | "getterName" | "groups" | "hidden" | "hydrate" | "ignoreSchemaChanges" | "index" | "lazy" | "length" | "name" | "nativeEnumName" | "nullable" | "onCreate" | "onUpdate" | "persist" | "precision" | "primary" | "ref" | "returning" | "runtimeType" | "scale" | "serializedName" | "serializedPrimaryKey" | "serializer" | "setter" | "type" | "unique" | "unsigned" | "version" | ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type")>;
|
|
16
|
+
readonly providerMessageId: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
|
|
17
|
+
nullable: true;
|
|
18
|
+
} & {
|
|
19
|
+
readonly '~c': true;
|
|
20
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
21
|
+
readonly error: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
|
|
22
|
+
nullable: true;
|
|
23
|
+
} & {
|
|
24
|
+
readonly '~c': true;
|
|
25
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
26
|
+
readonly metadata: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<unknown, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
|
|
27
|
+
nullable: true;
|
|
28
|
+
} & {
|
|
29
|
+
readonly '~c': true;
|
|
30
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
31
|
+
}, undefined, never, never, false>, never, {
|
|
32
|
+
readonly id: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "primary"> & {
|
|
33
|
+
primary: true;
|
|
34
|
+
} & {
|
|
35
|
+
readonly '~c': true;
|
|
36
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
37
|
+
readonly to: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, import("@mikro-orm/core").EmptyOptions & {
|
|
38
|
+
readonly '~c': true;
|
|
39
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
40
|
+
readonly body: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, import("@mikro-orm/core").EmptyOptions & {
|
|
41
|
+
readonly '~c': true;
|
|
42
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
43
|
+
readonly status: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<any, import("@mikro-orm/core").EmptyOptions & {
|
|
44
|
+
readonly '~c': true;
|
|
45
|
+
}, "accessor" | "array" | "autoincrement" | "check" | "collation" | "columnType" | "columnTypes" | "comment" | "compliance" | "concurrencyCheck" | "customOrder" | "default" | "defaultRaw" | "extra" | "fieldName" | "fieldNames" | "formula" | "generated" | "getter" | "getterName" | "groups" | "hidden" | "hydrate" | "ignoreSchemaChanges" | "index" | "lazy" | "length" | "name" | "nativeEnumName" | "nullable" | "onCreate" | "onUpdate" | "persist" | "precision" | "primary" | "ref" | "returning" | "runtimeType" | "scale" | "serializedName" | "serializedPrimaryKey" | "serializer" | "setter" | "type" | "unique" | "unsigned" | "version" | ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type")>, "accessor" | "array" | "autoincrement" | "check" | "collation" | "columnType" | "columnTypes" | "comment" | "compliance" | "concurrencyCheck" | "customOrder" | "default" | "defaultRaw" | "extra" | "fieldName" | "fieldNames" | "formula" | "generated" | "getter" | "getterName" | "groups" | "hidden" | "hydrate" | "ignoreSchemaChanges" | "index" | "lazy" | "length" | "name" | "nativeEnumName" | "nullable" | "onCreate" | "onUpdate" | "persist" | "precision" | "primary" | "ref" | "returning" | "runtimeType" | "scale" | "serializedName" | "serializedPrimaryKey" | "serializer" | "setter" | "type" | "unique" | "unsigned" | "version" | ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type")>;
|
|
46
|
+
readonly providerMessageId: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
|
|
47
|
+
nullable: true;
|
|
48
|
+
} & {
|
|
49
|
+
readonly '~c': true;
|
|
50
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
51
|
+
readonly error: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
|
|
52
|
+
nullable: true;
|
|
53
|
+
} & {
|
|
54
|
+
readonly '~c': true;
|
|
55
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
56
|
+
readonly metadata: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<unknown, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
|
|
57
|
+
nullable: true;
|
|
58
|
+
} & {
|
|
59
|
+
readonly '~c': true;
|
|
60
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
61
|
+
}, import("@mikro-orm/core").EntityCtor<import("@mikro-orm/core").InferEntityFromProperties<{
|
|
62
|
+
readonly id: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "primary"> & {
|
|
63
|
+
primary: true;
|
|
64
|
+
} & {
|
|
65
|
+
readonly '~c': true;
|
|
66
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
67
|
+
readonly to: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, import("@mikro-orm/core").EmptyOptions & {
|
|
68
|
+
readonly '~c': true;
|
|
69
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
70
|
+
readonly body: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, import("@mikro-orm/core").EmptyOptions & {
|
|
71
|
+
readonly '~c': true;
|
|
72
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
73
|
+
readonly status: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<any, import("@mikro-orm/core").EmptyOptions & {
|
|
74
|
+
readonly '~c': true;
|
|
75
|
+
}, "accessor" | "array" | "autoincrement" | "check" | "collation" | "columnType" | "columnTypes" | "comment" | "compliance" | "concurrencyCheck" | "customOrder" | "default" | "defaultRaw" | "extra" | "fieldName" | "fieldNames" | "formula" | "generated" | "getter" | "getterName" | "groups" | "hidden" | "hydrate" | "ignoreSchemaChanges" | "index" | "lazy" | "length" | "name" | "nativeEnumName" | "nullable" | "onCreate" | "onUpdate" | "persist" | "precision" | "primary" | "ref" | "returning" | "runtimeType" | "scale" | "serializedName" | "serializedPrimaryKey" | "serializer" | "setter" | "type" | "unique" | "unsigned" | "version" | ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type")>, "accessor" | "array" | "autoincrement" | "check" | "collation" | "columnType" | "columnTypes" | "comment" | "compliance" | "concurrencyCheck" | "customOrder" | "default" | "defaultRaw" | "extra" | "fieldName" | "fieldNames" | "formula" | "generated" | "getter" | "getterName" | "groups" | "hidden" | "hydrate" | "ignoreSchemaChanges" | "index" | "lazy" | "length" | "name" | "nativeEnumName" | "nullable" | "onCreate" | "onUpdate" | "persist" | "precision" | "primary" | "ref" | "returning" | "runtimeType" | "scale" | "serializedName" | "serializedPrimaryKey" | "serializer" | "setter" | "type" | "unique" | "unsigned" | "version" | ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type")>;
|
|
76
|
+
readonly providerMessageId: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
|
|
77
|
+
nullable: true;
|
|
78
|
+
} & {
|
|
79
|
+
readonly '~c': true;
|
|
80
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
81
|
+
readonly error: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<string, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
|
|
82
|
+
nullable: true;
|
|
83
|
+
} & {
|
|
84
|
+
readonly '~c': true;
|
|
85
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
86
|
+
readonly metadata: Pick<import("@mikro-orm/core").UniversalPropertyOptionsBuilder<unknown, Omit<import("@mikro-orm/core").EmptyOptions, "nullable"> & {
|
|
87
|
+
nullable: true;
|
|
88
|
+
} & {
|
|
89
|
+
readonly '~c': true;
|
|
90
|
+
}, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>, ("$type" | "lazyRef" | "strictNullable" | "~options" | "~type") | keyof import("@mikro-orm/core").PropertyOptions<any>>;
|
|
91
|
+
}, undefined, never, never, false>>, undefined>;
|
|
92
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../persistence/entities/index.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+CAWpB,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// persistence/entities/index.ts
|
|
21
|
+
var entities_exports = {};
|
|
22
|
+
__export(entities_exports, {
|
|
23
|
+
SmsRecord: () => SmsRecord
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(entities_exports);
|
|
26
|
+
var import_persistence = require("@forklaunch/core/persistence");
|
|
27
|
+
var SmsRecord = (0, import_persistence.defineComplianceEntity)({
|
|
28
|
+
name: "SmsRecord",
|
|
29
|
+
properties: {
|
|
30
|
+
id: import_persistence.fp.string().primary().compliance("none"),
|
|
31
|
+
to: import_persistence.fp.string().compliance("pii"),
|
|
32
|
+
body: import_persistence.fp.string().compliance("pii"),
|
|
33
|
+
status: import_persistence.fp.enum().compliance("none"),
|
|
34
|
+
providerMessageId: import_persistence.fp.string().nullable().compliance("none"),
|
|
35
|
+
error: import_persistence.fp.string().nullable().compliance("none"),
|
|
36
|
+
metadata: import_persistence.fp.json().nullable().compliance("none")
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
40
|
+
0 && (module.exports = {
|
|
41
|
+
SmsRecord
|
|
42
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// persistence/entities/index.ts
|
|
2
|
+
import { defineComplianceEntity, fp } from "@forklaunch/core/persistence";
|
|
3
|
+
var SmsRecord = defineComplianceEntity({
|
|
4
|
+
name: "SmsRecord",
|
|
5
|
+
properties: {
|
|
6
|
+
id: fp.string().primary().compliance("none"),
|
|
7
|
+
to: fp.string().compliance("pii"),
|
|
8
|
+
body: fp.string().compliance("pii"),
|
|
9
|
+
status: fp.enum().compliance("none"),
|
|
10
|
+
providerMessageId: fp.string().nullable().compliance("none"),
|
|
11
|
+
error: fp.string().nullable().compliance("none"),
|
|
12
|
+
metadata: fp.json().nullable().compliance("none")
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
export {
|
|
16
|
+
SmsRecord
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../services/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAE9B,cAAc,6CAA6C,CAAC;AAC5D,cAAc,wCAAwC,CAAC"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// services/index.ts
|
|
22
|
+
var services_exports = {};
|
|
23
|
+
__export(services_exports, {
|
|
24
|
+
TwilioSmsService: () => TwilioSmsService,
|
|
25
|
+
mapTwilioMessageStatus: () => mapTwilioMessageStatus
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(services_exports);
|
|
28
|
+
|
|
29
|
+
// services/sms.service.ts
|
|
30
|
+
var import_http = require("@forklaunch/core/http");
|
|
31
|
+
var import_enum = require("@forklaunch/implementation-messaging-base/enum");
|
|
32
|
+
var import_core = require("@mikro-orm/core");
|
|
33
|
+
|
|
34
|
+
// domain/enum/twilioMessageStatus.enum.ts
|
|
35
|
+
var TwilioMessageStatusEnum = {
|
|
36
|
+
QUEUED: "queued",
|
|
37
|
+
ACCEPTED: "accepted",
|
|
38
|
+
SCHEDULED: "scheduled",
|
|
39
|
+
CANCELED: "canceled",
|
|
40
|
+
SENDING: "sending",
|
|
41
|
+
SENT: "sent",
|
|
42
|
+
DELIVERED: "delivered",
|
|
43
|
+
UNDELIVERED: "undelivered",
|
|
44
|
+
FAILED: "failed",
|
|
45
|
+
RECEIVING: "receiving",
|
|
46
|
+
RECEIVED: "received",
|
|
47
|
+
READ: "read"
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// services/sms.service.ts
|
|
51
|
+
var mapTwilioMessageStatus = (messageStatus) => {
|
|
52
|
+
switch (messageStatus) {
|
|
53
|
+
case TwilioMessageStatusEnum.SENT:
|
|
54
|
+
return import_enum.SmsStatusEnum.SENT;
|
|
55
|
+
case TwilioMessageStatusEnum.DELIVERED:
|
|
56
|
+
case TwilioMessageStatusEnum.READ:
|
|
57
|
+
return import_enum.SmsStatusEnum.DELIVERED;
|
|
58
|
+
case TwilioMessageStatusEnum.UNDELIVERED:
|
|
59
|
+
return import_enum.SmsStatusEnum.UNDELIVERED;
|
|
60
|
+
case TwilioMessageStatusEnum.FAILED:
|
|
61
|
+
case TwilioMessageStatusEnum.CANCELED:
|
|
62
|
+
return import_enum.SmsStatusEnum.FAILED;
|
|
63
|
+
case TwilioMessageStatusEnum.QUEUED:
|
|
64
|
+
case TwilioMessageStatusEnum.ACCEPTED:
|
|
65
|
+
case TwilioMessageStatusEnum.SCHEDULED:
|
|
66
|
+
case TwilioMessageStatusEnum.SENDING:
|
|
67
|
+
default:
|
|
68
|
+
return import_enum.SmsStatusEnum.QUEUED;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var TWILIO_API_BASE_URL = "https://api.twilio.com/2010-04-01";
|
|
72
|
+
var TwilioSmsService = class {
|
|
73
|
+
constructor(accountSid, authToken, fromNumber, em, openTelemetryCollector, schemaValidator, mappers, options) {
|
|
74
|
+
this.options = options;
|
|
75
|
+
this.accountSid = accountSid;
|
|
76
|
+
this.authToken = authToken;
|
|
77
|
+
this.fromNumber = fromNumber;
|
|
78
|
+
this.em = em;
|
|
79
|
+
this.openTelemetryCollector = openTelemetryCollector;
|
|
80
|
+
this.schemaValidator = schemaValidator;
|
|
81
|
+
this.mappers = mappers;
|
|
82
|
+
this.evaluatedTelemetryOptions = options?.telemetry ? (0, import_http.evaluateTelemetryOptions)(options.telemetry).enabled : {
|
|
83
|
+
logging: false,
|
|
84
|
+
metrics: false,
|
|
85
|
+
tracing: false
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
options;
|
|
89
|
+
evaluatedTelemetryOptions;
|
|
90
|
+
accountSid;
|
|
91
|
+
authToken;
|
|
92
|
+
fromNumber;
|
|
93
|
+
em;
|
|
94
|
+
openTelemetryCollector;
|
|
95
|
+
schemaValidator;
|
|
96
|
+
mappers;
|
|
97
|
+
async sendSms(smsDto, em, ...args) {
|
|
98
|
+
if (this.evaluatedTelemetryOptions.logging) {
|
|
99
|
+
this.openTelemetryCollector.info("Sending sms via twilio", {
|
|
100
|
+
to: smsDto.to
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const smsRecord = await this.mappers.SendSmsMapper.toEntity(
|
|
104
|
+
smsDto,
|
|
105
|
+
em ?? this.em,
|
|
106
|
+
...args[0] instanceof import_core.EntityManager ? args.slice(1) : args
|
|
107
|
+
);
|
|
108
|
+
const record = smsRecord;
|
|
109
|
+
const response = await fetch(
|
|
110
|
+
`${TWILIO_API_BASE_URL}/Accounts/${this.accountSid}/Messages.json`,
|
|
111
|
+
{
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Basic ${Buffer.from(
|
|
115
|
+
`${this.accountSid}:${this.authToken}`
|
|
116
|
+
).toString("base64")}`,
|
|
117
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
118
|
+
},
|
|
119
|
+
body: new URLSearchParams({
|
|
120
|
+
To: smsDto.to,
|
|
121
|
+
From: this.fromNumber,
|
|
122
|
+
Body: smsDto.body
|
|
123
|
+
}).toString()
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
if (response.ok) {
|
|
127
|
+
const message = await response.json();
|
|
128
|
+
record.providerMessageId = message.sid;
|
|
129
|
+
record.status = mapTwilioMessageStatus(message.status);
|
|
130
|
+
record.error = message.error_message ?? null;
|
|
131
|
+
} else {
|
|
132
|
+
const errorBody = await response.text();
|
|
133
|
+
record.status = import_enum.SmsStatusEnum.FAILED;
|
|
134
|
+
record.error = `Twilio API error (${response.status}): ${errorBody}`;
|
|
135
|
+
this.openTelemetryCollector.error("Twilio message dispatch failed", {
|
|
136
|
+
status: response.status,
|
|
137
|
+
error: errorBody
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
await (em ?? this.em).transactional(async (innerEm) => {
|
|
141
|
+
await innerEm.persist(smsRecord);
|
|
142
|
+
});
|
|
143
|
+
return this.mappers.SmsRecordMapper.toDto(
|
|
144
|
+
smsRecord
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
async getSmsRecord({ id }, em) {
|
|
148
|
+
if (this.evaluatedTelemetryOptions.logging) {
|
|
149
|
+
this.openTelemetryCollector.info("Getting sms record", { id });
|
|
150
|
+
}
|
|
151
|
+
const smsRecord = await (em ?? this.em).findOneOrFail(
|
|
152
|
+
this.mappers.SmsRecordMapper.entity,
|
|
153
|
+
{ id }
|
|
154
|
+
);
|
|
155
|
+
return this.mappers.SmsRecordMapper.toDto(
|
|
156
|
+
smsRecord
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Maps a Twilio delivery-status callback (MessageStatus, MessageSid,
|
|
161
|
+
* ErrorMessage) onto the persisted record's status.
|
|
162
|
+
*/
|
|
163
|
+
async processStatusCallback(payload, em) {
|
|
164
|
+
if (this.evaluatedTelemetryOptions.logging) {
|
|
165
|
+
this.openTelemetryCollector.info("Processing twilio status callback", {
|
|
166
|
+
messageSid: payload.MessageSid,
|
|
167
|
+
messageStatus: payload.MessageStatus
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const smsRecord = await (em ?? this.em).findOneOrFail(
|
|
171
|
+
this.mappers.SmsRecordMapper.entity,
|
|
172
|
+
{ providerMessageId: payload.MessageSid }
|
|
173
|
+
);
|
|
174
|
+
const record = smsRecord;
|
|
175
|
+
record.status = mapTwilioMessageStatus(payload.MessageStatus);
|
|
176
|
+
record.error = payload.ErrorMessage ?? null;
|
|
177
|
+
await (em ?? this.em).flush();
|
|
178
|
+
return this.mappers.SmsRecordMapper.toDto(
|
|
179
|
+
smsRecord
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// services/index.ts
|
|
185
|
+
__reExport(services_exports, require("@forklaunch/interfaces-messaging/interfaces"), module.exports);
|
|
186
|
+
__reExport(services_exports, require("@forklaunch/interfaces-messaging/types"), module.exports);
|
|
187
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
188
|
+
0 && (module.exports = {
|
|
189
|
+
TwilioSmsService,
|
|
190
|
+
mapTwilioMessageStatus,
|
|
191
|
+
...require("@forklaunch/interfaces-messaging/interfaces"),
|
|
192
|
+
...require("@forklaunch/interfaces-messaging/types")
|
|
193
|
+
});
|