vintasend-api 0.0.0-stage → 1.0.0-alpha6
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/README.md +273 -2
- package/dist/app.d.ts +33 -0
- package/dist/app.js +33 -0
- package/dist/app.js.map +1 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -0
- package/dist/contract/types.d.ts +196 -0
- package/dist/contract/types.js +12 -0
- package/dist/contract/types.js.map +1 -0
- package/dist/domain/capabilities.d.ts +13 -0
- package/dist/domain/capabilities.js +19 -0
- package/dist/domain/capabilities.js.map +1 -0
- package/dist/domain/filters.d.ts +16 -0
- package/dist/domain/filters.js +82 -0
- package/dist/domain/filters.js.map +1 -0
- package/dist/domain/pagination.d.ts +20 -0
- package/dist/domain/pagination.js +24 -0
- package/dist/domain/pagination.js.map +1 -0
- package/dist/domain/schemas.d.ts +90 -0
- package/dist/domain/schemas.js +52 -0
- package/dist/domain/schemas.js.map +1 -0
- package/dist/domain/serialize.d.ts +19 -0
- package/dist/domain/serialize.js +101 -0
- package/dist/domain/serialize.js.map +1 -0
- package/dist/errors.d.ts +45 -0
- package/dist/errors.js +94 -0
- package/dist/errors.js.map +1 -0
- package/dist/exports.d.ts +14 -0
- package/dist/exports.js +14 -0
- package/dist/exports.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/authenticate.d.ts +34 -0
- package/dist/middleware/authenticate.js +68 -0
- package/dist/middleware/authenticate.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +35 -0
- package/dist/middleware/error-handler.js +89 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/routes/notifications.d.ts +14 -0
- package/dist/routes/notifications.js +119 -0
- package/dist/routes/notifications.js.map +1 -0
- package/dist/routes/validation.d.ts +14 -0
- package/dist/routes/validation.js +44 -0
- package/dist/routes/validation.js.map +1 -0
- package/dist/services/github-template-client.d.ts +26 -0
- package/dist/services/github-template-client.js +148 -0
- package/dist/services/github-template-client.js.map +1 -0
- package/dist/services/github-template-preview-config.d.ts +8 -0
- package/dist/services/github-template-preview-config.js +51 -0
- package/dist/services/github-template-preview-config.js.map +1 -0
- package/dist/services/notification-preview.d.ts +22 -0
- package/dist/services/notification-preview.js +66 -0
- package/dist/services/notification-preview.js.map +1 -0
- package/dist/services/notification-service-port.d.ts +40 -0
- package/dist/services/notification-service-port.js +19 -0
- package/dist/services/notification-service-port.js.map +1 -0
- package/dist/services/paged-notification-reader.d.ts +22 -0
- package/dist/services/paged-notification-reader.js +36 -0
- package/dist/services/paged-notification-reader.js.map +1 -0
- package/dist/services/service-loader.d.ts +15 -0
- package/dist/services/service-loader.js +56 -0
- package/dist/services/service-loader.js.map +1 -0
- package/dist/services/template-path-resolver.d.ts +4 -0
- package/dist/services/template-path-resolver.js +17 -0
- package/dist/services/template-path-resolver.js.map +1 -0
- package/openapi.yaml +666 -0
- package/package.json +66 -4
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability map is a backend report, and only part of it is any of a
|
|
3
|
+
* client's business.
|
|
4
|
+
*
|
|
5
|
+
* `/api/v1/capabilities` exists so consumers can hide sorting and filtering
|
|
6
|
+
* affordances the backend cannot honour. Pagination conventions are not that:
|
|
7
|
+
* the wire contract is unconditionally 1-indexed and this server does the
|
|
8
|
+
* conversion, so publishing `pagination.oneIndexed` would only invite a client
|
|
9
|
+
* to convert a second time.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Capability namespaces that describe how the server talks to its backend,
|
|
13
|
+
* rather than what a client may ask for.
|
|
14
|
+
*/
|
|
15
|
+
const BACKEND_ONLY_CAPABILITY_PREFIXES = ['pagination.'];
|
|
16
|
+
export function toWireCapabilities(capabilities) {
|
|
17
|
+
return Object.fromEntries(Object.entries(capabilities).filter(([key]) => !BACKEND_ONLY_CAPABILITY_PREFIXES.some((prefix) => key.startsWith(prefix))));
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=capabilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../../src/domain/capabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH;;;GAGG;AACH,MAAM,gCAAgC,GAAG,CAAC,aAAa,CAAC,CAAC;AAEzD,MAAM,UAAU,kBAAkB,CAChC,YAA4C;IAE5C,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,MAAM,CACjC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,gCAAgC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CACtF,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translates validated query parameters into VintaSend backend filters.
|
|
3
|
+
*
|
|
4
|
+
* String filters and ordering are negotiated against the backend's advertised
|
|
5
|
+
* capabilities: a backend that cannot do case-insensitive `includes` gets an
|
|
6
|
+
* exact match instead, and ordering by an unsupported field is dropped rather
|
|
7
|
+
* than failing the request.
|
|
8
|
+
*/
|
|
9
|
+
import type { NotificationFilterCapabilities, NotificationOrderBy, StringFieldFilter } from 'vintasend';
|
|
10
|
+
import type { ApiNotificationFilterFields } from '../services/notification-service-port.js';
|
|
11
|
+
import type { NotificationListQueryInput } from './schemas.js';
|
|
12
|
+
export declare const DEFAULT_ORDER_BY_FIELD: "createdAt";
|
|
13
|
+
export declare const DEFAULT_ORDER_BY_DIRECTION: "desc";
|
|
14
|
+
export declare function buildStringFilter(value: string, capabilities: NotificationFilterCapabilities): StringFieldFilter;
|
|
15
|
+
export declare function buildBackendFilter(query: NotificationListQueryInput, capabilities: NotificationFilterCapabilities): ApiNotificationFilterFields;
|
|
16
|
+
export declare function buildOrderBy(query: NotificationListQueryInput, capabilities: NotificationFilterCapabilities): NotificationOrderBy | undefined;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translates validated query parameters into VintaSend backend filters.
|
|
3
|
+
*
|
|
4
|
+
* String filters and ordering are negotiated against the backend's advertised
|
|
5
|
+
* capabilities: a backend that cannot do case-insensitive `includes` gets an
|
|
6
|
+
* exact match instead, and ordering by an unsupported field is dropped rather
|
|
7
|
+
* than failing the request.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_ORDER_BY_FIELD = 'createdAt';
|
|
10
|
+
export const DEFAULT_ORDER_BY_DIRECTION = 'desc';
|
|
11
|
+
export function buildStringFilter(value, capabilities) {
|
|
12
|
+
const supportsIncludes = capabilities['stringLookups.includes'];
|
|
13
|
+
const supportsCaseInsensitive = capabilities['stringLookups.caseInsensitive'];
|
|
14
|
+
if (supportsIncludes) {
|
|
15
|
+
return {
|
|
16
|
+
lookup: 'includes',
|
|
17
|
+
value,
|
|
18
|
+
...(supportsCaseInsensitive ? { caseSensitive: false } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (supportsCaseInsensitive) {
|
|
22
|
+
return { lookup: 'exact', value, caseSensitive: false };
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
export function buildBackendFilter(query, capabilities) {
|
|
27
|
+
const filter = {};
|
|
28
|
+
if (query.status)
|
|
29
|
+
filter.status = query.status;
|
|
30
|
+
if (query.notificationType)
|
|
31
|
+
filter.notificationType = query.notificationType;
|
|
32
|
+
if (query.adapterUsed)
|
|
33
|
+
filter.adapterUsed = query.adapterUsed;
|
|
34
|
+
if (query.userId)
|
|
35
|
+
filter.userId = query.userId;
|
|
36
|
+
if (query.tenant)
|
|
37
|
+
filter.tenant = query.tenant;
|
|
38
|
+
// Compared against undefined rather than tested for truthiness: version 0 is a legitimate value
|
|
39
|
+
// the query schema accepts, and `if (query.requestedTemplateVersion)` would drop it.
|
|
40
|
+
//
|
|
41
|
+
// Not capability-gated, matching the Python implementation. Both fields default to unsupported
|
|
42
|
+
// in VintaSend's capability map, so gating them here would silently drop the filter for every
|
|
43
|
+
// backend that has not opted in — and a filter the backend ignores returns too many rows rather
|
|
44
|
+
// than too few, which a caller can see. Read `/capabilities` to know whether it will bite.
|
|
45
|
+
if (query.requestedTemplateVersion !== undefined) {
|
|
46
|
+
filter.requestedTemplateVersion = query.requestedTemplateVersion;
|
|
47
|
+
}
|
|
48
|
+
if (query.usedTemplateVersion !== undefined) {
|
|
49
|
+
filter.usedTemplateVersion = query.usedTemplateVersion;
|
|
50
|
+
}
|
|
51
|
+
if (query.bodyTemplate) {
|
|
52
|
+
filter.bodyTemplate = buildStringFilter(query.bodyTemplate, capabilities);
|
|
53
|
+
}
|
|
54
|
+
if (query.subjectTemplate) {
|
|
55
|
+
filter.subjectTemplate = buildStringFilter(query.subjectTemplate, capabilities);
|
|
56
|
+
}
|
|
57
|
+
if (query.contextName) {
|
|
58
|
+
filter.contextName = buildStringFilter(query.contextName, capabilities);
|
|
59
|
+
}
|
|
60
|
+
if (query.createdAtFrom || query.createdAtTo) {
|
|
61
|
+
filter.createdAtRange = {
|
|
62
|
+
...(query.createdAtFrom ? { from: new Date(query.createdAtFrom) } : {}),
|
|
63
|
+
...(query.createdAtTo ? { to: new Date(query.createdAtTo) } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (query.sentAtFrom || query.sentAtTo) {
|
|
67
|
+
filter.sentAtRange = {
|
|
68
|
+
...(query.sentAtFrom ? { from: new Date(query.sentAtFrom) } : {}),
|
|
69
|
+
...(query.sentAtTo ? { to: new Date(query.sentAtTo) } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return filter;
|
|
73
|
+
}
|
|
74
|
+
export function buildOrderBy(query, capabilities) {
|
|
75
|
+
const field = query.orderByField ?? DEFAULT_ORDER_BY_FIELD;
|
|
76
|
+
const direction = query.orderByDirection ?? DEFAULT_ORDER_BY_DIRECTION;
|
|
77
|
+
if (capabilities[`orderBy.${field}`] === false) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
return { field, direction };
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=filters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filters.js","sourceRoot":"","sources":["../../src/domain/filters.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-number translation between the wire contract and the backend.
|
|
3
|
+
*
|
|
4
|
+
* The API is always 1-indexed: `page=1` is the first page, in every
|
|
5
|
+
* implementation of this contract. Backends are not: the TypeScript VintaSend
|
|
6
|
+
* backends are 0-indexed (`vintasend-prisma` and `vintasend-medplum` both
|
|
7
|
+
* translate `page` as `page * pageSize`), while the Python ones are 1-indexed.
|
|
8
|
+
* The offset therefore comes from the backend's `pagination.oneIndexed`
|
|
9
|
+
* capability rather than from a hardcoded assumption.
|
|
10
|
+
*
|
|
11
|
+
* A backend that reports no capability at all falls back to this library's own
|
|
12
|
+
* convention, 0-indexed. In practice the key is always present: VintaSend merges
|
|
13
|
+
* its defaults under the backend's report.
|
|
14
|
+
*/
|
|
15
|
+
import type { NotificationFilterCapabilities } from 'vintasend';
|
|
16
|
+
export declare function isOneIndexedBackend(capabilities: NotificationFilterCapabilities): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Converts a 1-indexed page from the contract into the backend's own numbering.
|
|
19
|
+
*/
|
|
20
|
+
export declare function toBackendPage(page: number, capabilities: NotificationFilterCapabilities): number;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-number translation between the wire contract and the backend.
|
|
3
|
+
*
|
|
4
|
+
* The API is always 1-indexed: `page=1` is the first page, in every
|
|
5
|
+
* implementation of this contract. Backends are not: the TypeScript VintaSend
|
|
6
|
+
* backends are 0-indexed (`vintasend-prisma` and `vintasend-medplum` both
|
|
7
|
+
* translate `page` as `page * pageSize`), while the Python ones are 1-indexed.
|
|
8
|
+
* The offset therefore comes from the backend's `pagination.oneIndexed`
|
|
9
|
+
* capability rather than from a hardcoded assumption.
|
|
10
|
+
*
|
|
11
|
+
* A backend that reports no capability at all falls back to this library's own
|
|
12
|
+
* convention, 0-indexed. In practice the key is always present: VintaSend merges
|
|
13
|
+
* its defaults under the backend's report.
|
|
14
|
+
*/
|
|
15
|
+
export function isOneIndexedBackend(capabilities) {
|
|
16
|
+
return capabilities['pagination.oneIndexed'] === true;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Converts a 1-indexed page from the contract into the backend's own numbering.
|
|
20
|
+
*/
|
|
21
|
+
export function toBackendPage(page, capabilities) {
|
|
22
|
+
return isOneIndexedBackend(capabilities) ? page : page - 1;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=pagination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pagination.js","sourceRoot":"","sources":["../../src/domain/pagination.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,UAAU,mBAAmB,CAAC,YAA4C;IAC9E,OAAO,YAAY,CAAC,uBAAuB,CAAC,KAAK,IAAI,CAAC;AACxD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,YAA4C;IACtF,OAAO,mBAAmB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;AAC7D,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request validation schemas. These define, precisely, what the API accepts —
|
|
3
|
+
* an implementation of this contract in another language should reject the same
|
|
4
|
+
* inputs with the same 400 responses.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
export declare const DEFAULT_PAGE = 1;
|
|
8
|
+
export declare const DEFAULT_PAGE_SIZE = 20;
|
|
9
|
+
export declare const MIN_PAGE_SIZE = 1;
|
|
10
|
+
export declare const MAX_PAGE_SIZE = 100;
|
|
11
|
+
export declare const paginationQuerySchema: z.ZodObject<{
|
|
12
|
+
page: z.ZodDefault<z.ZodNumber>;
|
|
13
|
+
pageSize: z.ZodDefault<z.ZodNumber>;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
page: number;
|
|
16
|
+
pageSize: number;
|
|
17
|
+
}, {
|
|
18
|
+
page?: number | undefined;
|
|
19
|
+
pageSize?: number | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export declare const notificationListQuerySchema: z.ZodObject<{
|
|
22
|
+
page: z.ZodDefault<z.ZodNumber>;
|
|
23
|
+
pageSize: z.ZodDefault<z.ZodNumber>;
|
|
24
|
+
} & {
|
|
25
|
+
status: z.ZodOptional<z.ZodEnum<["PENDING_SEND", "SENT", "FAILED", "READ", "CANCELLED"]>>;
|
|
26
|
+
notificationType: z.ZodOptional<z.ZodEnum<["EMAIL", "SMS", "PUSH", "IN_APP"]>>;
|
|
27
|
+
adapterUsed: z.ZodOptional<z.ZodString>;
|
|
28
|
+
userId: z.ZodOptional<z.ZodString>;
|
|
29
|
+
bodyTemplate: z.ZodOptional<z.ZodString>;
|
|
30
|
+
subjectTemplate: z.ZodOptional<z.ZodString>;
|
|
31
|
+
contextName: z.ZodOptional<z.ZodString>;
|
|
32
|
+
tenant: z.ZodOptional<z.ZodString>;
|
|
33
|
+
requestedTemplateVersion: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
usedTemplateVersion: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
createdAtFrom: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
36
|
+
createdAtTo: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
37
|
+
sentAtFrom: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
38
|
+
sentAtTo: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
39
|
+
orderByField: z.ZodOptional<z.ZodEnum<["sendAfter", "sentAt", "readAt", "createdAt", "updatedAt"]>>;
|
|
40
|
+
orderByDirection: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
|
|
41
|
+
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
page: number;
|
|
43
|
+
pageSize: number;
|
|
44
|
+
status?: "PENDING_SEND" | "SENT" | "FAILED" | "READ" | "CANCELLED" | undefined;
|
|
45
|
+
notificationType?: "EMAIL" | "SMS" | "PUSH" | "IN_APP" | undefined;
|
|
46
|
+
adapterUsed?: string | undefined;
|
|
47
|
+
userId?: string | undefined;
|
|
48
|
+
bodyTemplate?: string | undefined;
|
|
49
|
+
subjectTemplate?: string | undefined;
|
|
50
|
+
contextName?: string | undefined;
|
|
51
|
+
tenant?: string | undefined;
|
|
52
|
+
requestedTemplateVersion?: number | undefined;
|
|
53
|
+
usedTemplateVersion?: number | undefined;
|
|
54
|
+
createdAtFrom?: string | undefined;
|
|
55
|
+
createdAtTo?: string | undefined;
|
|
56
|
+
sentAtFrom?: string | undefined;
|
|
57
|
+
sentAtTo?: string | undefined;
|
|
58
|
+
orderByField?: "sendAfter" | "sentAt" | "readAt" | "createdAt" | "updatedAt" | undefined;
|
|
59
|
+
orderByDirection?: "asc" | "desc" | undefined;
|
|
60
|
+
}, {
|
|
61
|
+
page?: number | undefined;
|
|
62
|
+
pageSize?: number | undefined;
|
|
63
|
+
status?: "PENDING_SEND" | "SENT" | "FAILED" | "READ" | "CANCELLED" | undefined;
|
|
64
|
+
notificationType?: "EMAIL" | "SMS" | "PUSH" | "IN_APP" | undefined;
|
|
65
|
+
adapterUsed?: string | undefined;
|
|
66
|
+
userId?: string | undefined;
|
|
67
|
+
bodyTemplate?: string | undefined;
|
|
68
|
+
subjectTemplate?: string | undefined;
|
|
69
|
+
contextName?: string | undefined;
|
|
70
|
+
tenant?: string | undefined;
|
|
71
|
+
requestedTemplateVersion?: number | undefined;
|
|
72
|
+
usedTemplateVersion?: number | undefined;
|
|
73
|
+
createdAtFrom?: string | undefined;
|
|
74
|
+
createdAtTo?: string | undefined;
|
|
75
|
+
sentAtFrom?: string | undefined;
|
|
76
|
+
sentAtTo?: string | undefined;
|
|
77
|
+
orderByField?: "sendAfter" | "sentAt" | "readAt" | "createdAt" | "updatedAt" | undefined;
|
|
78
|
+
orderByDirection?: "asc" | "desc" | undefined;
|
|
79
|
+
}>;
|
|
80
|
+
/** Body of `POST /notifications/{id}/resend`. Optional: an omitted body regenerates the context. */
|
|
81
|
+
export declare const resendBodySchema: z.ZodObject<{
|
|
82
|
+
useStoredContext: z.ZodDefault<z.ZodBoolean>;
|
|
83
|
+
}, "strip", z.ZodTypeAny, {
|
|
84
|
+
useStoredContext: boolean;
|
|
85
|
+
}, {
|
|
86
|
+
useStoredContext?: boolean | undefined;
|
|
87
|
+
}>;
|
|
88
|
+
export type NotificationListQueryInput = z.infer<typeof notificationListQuerySchema>;
|
|
89
|
+
export type PaginationQueryInput = z.infer<typeof paginationQuerySchema>;
|
|
90
|
+
export type ResendBodyInput = z.infer<typeof resendBodySchema>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request validation schemas. These define, precisely, what the API accepts —
|
|
3
|
+
* an implementation of this contract in another language should reject the same
|
|
4
|
+
* inputs with the same 400 responses.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
export const DEFAULT_PAGE = 1;
|
|
8
|
+
export const DEFAULT_PAGE_SIZE = 20;
|
|
9
|
+
export const MIN_PAGE_SIZE = 1;
|
|
10
|
+
export const MAX_PAGE_SIZE = 100;
|
|
11
|
+
const notificationStatusSchema = z.enum(['PENDING_SEND', 'SENT', 'FAILED', 'READ', 'CANCELLED']);
|
|
12
|
+
const notificationTypeSchema = z.enum(['EMAIL', 'SMS', 'PUSH', 'IN_APP']);
|
|
13
|
+
const orderByFieldSchema = z.enum(['sendAfter', 'sentAt', 'readAt', 'createdAt', 'updatedAt']);
|
|
14
|
+
const orderByDirectionSchema = z.enum(['asc', 'desc']);
|
|
15
|
+
const isoDateSchema = z.string().refine((value) => !Number.isNaN(Date.parse(value)), {
|
|
16
|
+
message: 'Must be an ISO-8601 date string',
|
|
17
|
+
});
|
|
18
|
+
const nonEmptyString = z.string().trim().min(1);
|
|
19
|
+
export const paginationQuerySchema = z.object({
|
|
20
|
+
page: z.coerce.number().int().min(1).default(DEFAULT_PAGE),
|
|
21
|
+
pageSize: z.coerce
|
|
22
|
+
.number()
|
|
23
|
+
.int()
|
|
24
|
+
.min(MIN_PAGE_SIZE)
|
|
25
|
+
.max(MAX_PAGE_SIZE)
|
|
26
|
+
.default(DEFAULT_PAGE_SIZE),
|
|
27
|
+
});
|
|
28
|
+
export const notificationListQuerySchema = paginationQuerySchema.extend({
|
|
29
|
+
status: notificationStatusSchema.optional(),
|
|
30
|
+
notificationType: notificationTypeSchema.optional(),
|
|
31
|
+
adapterUsed: nonEmptyString.optional(),
|
|
32
|
+
userId: nonEmptyString.optional(),
|
|
33
|
+
bodyTemplate: nonEmptyString.optional(),
|
|
34
|
+
subjectTemplate: nonEmptyString.optional(),
|
|
35
|
+
contextName: nonEmptyString.optional(),
|
|
36
|
+
tenant: nonEmptyString.optional(),
|
|
37
|
+
// `min(0)` rather than `min(1)`: the contract admits version 0, and rejecting it here would
|
|
38
|
+
// make a legal filter a 400.
|
|
39
|
+
requestedTemplateVersion: z.coerce.number().int().min(0).optional(),
|
|
40
|
+
usedTemplateVersion: z.coerce.number().int().min(0).optional(),
|
|
41
|
+
createdAtFrom: isoDateSchema.optional(),
|
|
42
|
+
createdAtTo: isoDateSchema.optional(),
|
|
43
|
+
sentAtFrom: isoDateSchema.optional(),
|
|
44
|
+
sentAtTo: isoDateSchema.optional(),
|
|
45
|
+
orderByField: orderByFieldSchema.optional(),
|
|
46
|
+
orderByDirection: orderByDirectionSchema.optional(),
|
|
47
|
+
});
|
|
48
|
+
/** Body of `POST /notifications/{id}/resend`. Optional: an omitted body regenerates the context. */
|
|
49
|
+
export const resendBodySchema = z.object({
|
|
50
|
+
useStoredContext: z.boolean().default(false),
|
|
51
|
+
});
|
|
52
|
+
//# sourceMappingURL=schemas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../src/domain/schemas.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts VintaSend database notifications into the wire contract.
|
|
3
|
+
*
|
|
4
|
+
* List payloads drop the potentially large context/attachment payloads; detail
|
|
5
|
+
* payloads keep them. Dates always become ISO-8601 strings, and absent dates
|
|
6
|
+
* are normalised to `null` (never `undefined`) so JSON responses are uniform.
|
|
7
|
+
*/
|
|
8
|
+
import type { Notification, NotificationDetail, OneOffNotification, UserNotification } from '../contract/types.js';
|
|
9
|
+
import type { ApiAnyDatabaseNotification, ApiDatabaseNotification, ApiDatabaseOneOffNotification } from '../services/notification-service-port.js';
|
|
10
|
+
export declare function serializeUserNotification(notification: ApiDatabaseNotification): UserNotification;
|
|
11
|
+
export declare function serializeOneOffNotification(notification: ApiDatabaseOneOffNotification): OneOffNotification;
|
|
12
|
+
/**
|
|
13
|
+
* Serializes either notification variant for list responses.
|
|
14
|
+
*/
|
|
15
|
+
export declare function serializeNotification(notification: ApiAnyDatabaseNotification): Notification;
|
|
16
|
+
/**
|
|
17
|
+
* Serializes either notification variant for detail responses.
|
|
18
|
+
*/
|
|
19
|
+
export declare function serializeNotificationDetail(notification: ApiAnyDatabaseNotification): NotificationDetail;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts VintaSend database notifications into the wire contract.
|
|
3
|
+
*
|
|
4
|
+
* List payloads drop the potentially large context/attachment payloads; detail
|
|
5
|
+
* payloads keep them. Dates always become ISO-8601 strings, and absent dates
|
|
6
|
+
* are normalised to `null` (never `undefined`) so JSON responses are uniform.
|
|
7
|
+
*/
|
|
8
|
+
import { isOneOffNotification } from 'vintasend';
|
|
9
|
+
function toIsoString(value) {
|
|
10
|
+
return value ? value.toISOString() : null;
|
|
11
|
+
}
|
|
12
|
+
function toJsonValue(value) {
|
|
13
|
+
if (value === undefined || value === null) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function serializeAttachments(attachments) {
|
|
19
|
+
if (!attachments) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return attachments.map((attachment) => ({
|
|
23
|
+
id: attachment.id,
|
|
24
|
+
filename: attachment.filename,
|
|
25
|
+
contentType: attachment.contentType,
|
|
26
|
+
size: attachment.size,
|
|
27
|
+
...(attachment.description === undefined ? {} : { description: attachment.description }),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
function serializeSharedFields(notification) {
|
|
31
|
+
return {
|
|
32
|
+
notificationType: notification.notificationType,
|
|
33
|
+
title: notification.title,
|
|
34
|
+
contextName: notification.contextName,
|
|
35
|
+
status: notification.status,
|
|
36
|
+
sendAfter: toIsoString(notification.sendAfter),
|
|
37
|
+
sentAt: toIsoString(notification.sentAt),
|
|
38
|
+
readAt: toIsoString(notification.readAt),
|
|
39
|
+
createdAt: toIsoString(notification.createdAt),
|
|
40
|
+
updatedAt: toIsoString(notification.updatedAt),
|
|
41
|
+
adapterUsed: notification.adapterUsed,
|
|
42
|
+
bodyTemplate: notification.bodyTemplate,
|
|
43
|
+
subjectTemplate: notification.subjectTemplate,
|
|
44
|
+
gitCommitSha: notification.gitCommitSha,
|
|
45
|
+
// `?? null` because a backend that does not store them leaves them undefined, and the
|
|
46
|
+
// contract types both as nullable rather than optional.
|
|
47
|
+
requestedTemplateVersion: notification.requestedTemplateVersion ?? null,
|
|
48
|
+
usedTemplateVersion: notification.usedTemplateVersion ?? null,
|
|
49
|
+
tenant: notification.tenant,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function serializeUserNotification(notification) {
|
|
53
|
+
return {
|
|
54
|
+
kind: 'user',
|
|
55
|
+
id: String(notification.id),
|
|
56
|
+
userId: String(notification.userId),
|
|
57
|
+
...serializeSharedFields(notification),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function serializeOneOffNotification(notification) {
|
|
61
|
+
return {
|
|
62
|
+
kind: 'one-off',
|
|
63
|
+
id: String(notification.id),
|
|
64
|
+
emailOrPhone: notification.emailOrPhone,
|
|
65
|
+
firstName: notification.firstName ?? null,
|
|
66
|
+
lastName: notification.lastName ?? null,
|
|
67
|
+
...serializeSharedFields(notification),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Serializes either notification variant for list responses.
|
|
72
|
+
*/
|
|
73
|
+
export function serializeNotification(notification) {
|
|
74
|
+
return isOneOffNotification(notification)
|
|
75
|
+
? serializeOneOffNotification(notification)
|
|
76
|
+
: serializeUserNotification(notification);
|
|
77
|
+
}
|
|
78
|
+
function serializeDetailFields(notification) {
|
|
79
|
+
return {
|
|
80
|
+
contextUsed: toJsonValue(notification.contextUsed),
|
|
81
|
+
contextParameters: toJsonValue(notification.contextParameters),
|
|
82
|
+
extraParams: toJsonValue(notification.extraParams),
|
|
83
|
+
attachments: serializeAttachments(notification.attachments),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Serializes either notification variant for detail responses.
|
|
88
|
+
*/
|
|
89
|
+
export function serializeNotificationDetail(notification) {
|
|
90
|
+
if (isOneOffNotification(notification)) {
|
|
91
|
+
return {
|
|
92
|
+
...serializeOneOffNotification(notification),
|
|
93
|
+
...serializeDetailFields(notification),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
...serializeUserNotification(notification),
|
|
98
|
+
...serializeDetailFields(notification),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=serialize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialize.js","sourceRoot":"","sources":["../../src/domain/serialize.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ApiErrorCode, ApiErrorIssue, ApiErrorResponse, JsonValue } from './contract/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Error carrying an API error code, which the error handler turns into the
|
|
4
|
+
* documented status code and error envelope.
|
|
5
|
+
*/
|
|
6
|
+
export declare class ApiError extends Error {
|
|
7
|
+
readonly code: ApiErrorCode;
|
|
8
|
+
readonly status: number;
|
|
9
|
+
readonly details?: JsonValue;
|
|
10
|
+
constructor(code: ApiErrorCode, message: string, details?: JsonValue);
|
|
11
|
+
static unauthorized(message: string): ApiError;
|
|
12
|
+
static forbidden(message: string): ApiError;
|
|
13
|
+
static notFound(message: string): ApiError;
|
|
14
|
+
static conflict(message: string): ApiError;
|
|
15
|
+
/**
|
|
16
|
+
* A 400, which always carries `details.issues`.
|
|
17
|
+
*
|
|
18
|
+
* Every invalid input answers in the same shape, so a client reads one list whatever it got
|
|
19
|
+
* wrong. A failure that is not about one field is a single issue with an empty path repeating
|
|
20
|
+
* the message. `context` adds keys next to `issues`.
|
|
21
|
+
*/
|
|
22
|
+
static badRequest(message: string, issues?: ApiErrorIssue[], context?: {
|
|
23
|
+
[key: string]: JsonValue;
|
|
24
|
+
}): ApiError;
|
|
25
|
+
toResponseBody(): ApiErrorResponse;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The 400 for input that failed validation, wherever in the request it was: a body field, a query
|
|
29
|
+
* or path parameter, or the body as a whole (an empty path).
|
|
30
|
+
*/
|
|
31
|
+
export declare function invalidRequest(issues: readonly {
|
|
32
|
+
path: readonly PropertyKey[];
|
|
33
|
+
message: string;
|
|
34
|
+
}[]): ApiError;
|
|
35
|
+
/**
|
|
36
|
+
* The contract error `error` stands for, or `undefined` when it is not one.
|
|
37
|
+
*
|
|
38
|
+
* An `ApiError` from this package is one. So is an error shaped like one — named `ApiError`,
|
|
39
|
+
* carrying a code this contract defines — because an `ApiError` class from another copy of this
|
|
40
|
+
* package, or from the other VintaSend API package, is not this class. That is the ordinary case
|
|
41
|
+
* for a host passing one `authenticate` to both APIs: whichever package it imported `ApiError`
|
|
42
|
+
* from, the other one sees a stranger, and an `instanceof` check would answer its 401 with a 500.
|
|
43
|
+
*/
|
|
44
|
+
export declare function asApiError(error: unknown): ApiError | undefined;
|
|
45
|
+
export declare function errorMessage(error: unknown): string;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `FORBIDDEN` is what an authenticator answers when it knows who the caller is and refuses them. A
|
|
3
|
+
* 401 there would tell a signed-in user to sign in again.
|
|
4
|
+
*/
|
|
5
|
+
const STATUS_BY_CODE = {
|
|
6
|
+
BAD_REQUEST: 400,
|
|
7
|
+
UNAUTHORIZED: 401,
|
|
8
|
+
FORBIDDEN: 403,
|
|
9
|
+
NOT_FOUND: 404,
|
|
10
|
+
CONFLICT: 409,
|
|
11
|
+
PREVIEW_UNAVAILABLE: 409,
|
|
12
|
+
UPSTREAM_ERROR: 502,
|
|
13
|
+
INTERNAL_ERROR: 500,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Error carrying an API error code, which the error handler turns into the
|
|
17
|
+
* documented status code and error envelope.
|
|
18
|
+
*/
|
|
19
|
+
export class ApiError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
status;
|
|
22
|
+
details;
|
|
23
|
+
constructor(code, message, details) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'ApiError';
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.status = STATUS_BY_CODE[code];
|
|
28
|
+
this.details = details;
|
|
29
|
+
}
|
|
30
|
+
static unauthorized(message) {
|
|
31
|
+
return new ApiError('UNAUTHORIZED', message);
|
|
32
|
+
}
|
|
33
|
+
static forbidden(message) {
|
|
34
|
+
return new ApiError('FORBIDDEN', message);
|
|
35
|
+
}
|
|
36
|
+
static notFound(message) {
|
|
37
|
+
return new ApiError('NOT_FOUND', message);
|
|
38
|
+
}
|
|
39
|
+
static conflict(message) {
|
|
40
|
+
return new ApiError('CONFLICT', message);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A 400, which always carries `details.issues`.
|
|
44
|
+
*
|
|
45
|
+
* Every invalid input answers in the same shape, so a client reads one list whatever it got
|
|
46
|
+
* wrong. A failure that is not about one field is a single issue with an empty path repeating
|
|
47
|
+
* the message. `context` adds keys next to `issues`.
|
|
48
|
+
*/
|
|
49
|
+
static badRequest(message, issues = [{ path: '', message }], context = {}) {
|
|
50
|
+
return new ApiError('BAD_REQUEST', message, { ...context, issues });
|
|
51
|
+
}
|
|
52
|
+
toResponseBody() {
|
|
53
|
+
return {
|
|
54
|
+
error: {
|
|
55
|
+
code: this.code,
|
|
56
|
+
message: this.message,
|
|
57
|
+
...(this.details === undefined ? {} : { details: this.details }),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The 400 for input that failed validation, wherever in the request it was: a body field, a query
|
|
64
|
+
* or path parameter, or the body as a whole (an empty path).
|
|
65
|
+
*/
|
|
66
|
+
export function invalidRequest(issues) {
|
|
67
|
+
return ApiError.badRequest('Invalid request.', issues.map((issue) => ({ path: issue.path.map(String).join('.'), message: issue.message })));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The contract error `error` stands for, or `undefined` when it is not one.
|
|
71
|
+
*
|
|
72
|
+
* An `ApiError` from this package is one. So is an error shaped like one — named `ApiError`,
|
|
73
|
+
* carrying a code this contract defines — because an `ApiError` class from another copy of this
|
|
74
|
+
* package, or from the other VintaSend API package, is not this class. That is the ordinary case
|
|
75
|
+
* for a host passing one `authenticate` to both APIs: whichever package it imported `ApiError`
|
|
76
|
+
* from, the other one sees a stranger, and an `instanceof` check would answer its 401 with a 500.
|
|
77
|
+
*/
|
|
78
|
+
export function asApiError(error) {
|
|
79
|
+
if (error instanceof ApiError) {
|
|
80
|
+
return error;
|
|
81
|
+
}
|
|
82
|
+
if (!(error instanceof Error) || error.name !== 'ApiError') {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const { code, details } = error;
|
|
86
|
+
if (typeof code !== 'string' || !Object.hasOwn(STATUS_BY_CODE, code)) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return new ApiError(code, error.message, details);
|
|
90
|
+
}
|
|
91
|
+
export function errorMessage(error) {
|
|
92
|
+
return error instanceof Error ? error.message : 'Unknown error';
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entrypoint for mounting the API inside a host's own server, and for sharing the wire
|
|
3
|
+
* contract with TypeScript clients.
|
|
4
|
+
*/
|
|
5
|
+
export { API_BASE_PATH, type AppDependencies, createApp } from './app.js';
|
|
6
|
+
export { loadServerConfig, type ServerConfig } from './config.js';
|
|
7
|
+
export * from './contract/types.js';
|
|
8
|
+
export { ApiError, invalidRequest } from './errors.js';
|
|
9
|
+
export { type Authenticated, type Authenticator, apiKeyAuthenticator, authenticated, } from './middleware/authenticate.js';
|
|
10
|
+
export { logUnhandledError, REQUEST_ID_HEADER, type UnhandledErrorHandler, } from './middleware/error-handler.js';
|
|
11
|
+
export { createGitHubTemplateClientFromEnv, GitHubTemplateClient, } from './services/github-template-client.js';
|
|
12
|
+
export type { TemplateSourceClient } from './services/notification-preview.js';
|
|
13
|
+
export { asNotificationServicePort, type NotificationServicePort, } from './services/notification-service-port.js';
|
|
14
|
+
export { createServiceProvider, loadNotificationService } from './services/service-loader.js';
|
package/dist/exports.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entrypoint for mounting the API inside a host's own server, and for sharing the wire
|
|
3
|
+
* contract with TypeScript clients.
|
|
4
|
+
*/
|
|
5
|
+
export { API_BASE_PATH, createApp } from './app.js';
|
|
6
|
+
export { loadServerConfig } from './config.js';
|
|
7
|
+
export * from './contract/types.js';
|
|
8
|
+
export { ApiError, invalidRequest } from './errors.js';
|
|
9
|
+
export { apiKeyAuthenticator, authenticated, } from './middleware/authenticate.js';
|
|
10
|
+
export { logUnhandledError, REQUEST_ID_HEADER, } from './middleware/error-handler.js';
|
|
11
|
+
export { createGitHubTemplateClientFromEnv, GitHubTemplateClient, } from './services/github-template-client.js';
|
|
12
|
+
export { asNotificationServicePort, } from './services/notification-service-port.js';
|
|
13
|
+
export { createServiceProvider, loadNotificationService } from './services/service-loader.js';
|
|
14
|
+
//# sourceMappingURL=exports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exports.js","sourceRoot":"","sources":["../src/exports.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAwB,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAqB,MAAM,aAAa,CAAC;AAClE,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAGL,mBAAmB,EACnB,aAAa,GACd,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,iBAAiB,EACjB,iBAAiB,GAElB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,iCAAiC,EACjC,oBAAoB,GACrB,MAAM,sCAAsC,CAAC;AAE9C,OAAO,EACL,yBAAyB,GAE1B,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC"}
|