@managemint-solutions/sdk 0.8.0 → 0.16.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/README.md +73 -1
- package/dist/additional-details/index.d.ts +3 -1
- package/dist/additional-details/index.js +15 -28
- package/dist/additional-details/index.js.map +1 -1
- package/dist/audit/dto.d.ts +13 -0
- package/dist/audit/dto.js +90 -0
- package/dist/audit/dto.js.map +1 -0
- package/dist/audit/index.d.ts +104 -0
- package/dist/audit/index.js +308 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/billing/dto.d.ts +33 -0
- package/dist/billing/dto.js +71 -0
- package/dist/billing/dto.js.map +1 -0
- package/dist/billing/index.d.ts +35 -0
- package/dist/billing/index.js +115 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.js +23 -7
- package/dist/client.js.map +1 -1
- package/dist/clients/index.d.ts +3 -1
- package/dist/clients/index.js +25 -62
- package/dist/clients/index.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/nest/index.d.ts +22 -0
- package/dist/nest/index.js +28 -1
- package/dist/nest/index.js.map +1 -1
- package/dist/notes/index.d.ts +3 -1
- package/dist/notes/index.js +16 -27
- package/dist/notes/index.js.map +1 -1
- package/dist/notifications/dto.d.ts +10 -0
- package/dist/notifications/dto.js +63 -0
- package/dist/notifications/dto.js.map +1 -0
- package/dist/notifications/index.d.ts +139 -0
- package/dist/notifications/index.js +354 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/organizations/dto.d.ts +62 -0
- package/dist/organizations/dto.js +445 -0
- package/dist/organizations/dto.js.map +1 -0
- package/dist/organizations/index.d.ts +72 -0
- package/dist/organizations/index.js +221 -0
- package/dist/organizations/index.js.map +1 -0
- package/dist/permissions/dto.d.ts +27 -0
- package/dist/permissions/dto.js +215 -0
- package/dist/permissions/dto.js.map +1 -0
- package/dist/permissions/index.d.ts +36 -0
- package/dist/permissions/index.js +110 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/projects/index.d.ts +3 -1
- package/dist/projects/index.js +25 -62
- package/dist/projects/index.js.map +1 -1
- package/dist/service-client/index.d.ts +72 -0
- package/dist/service-client/index.js +119 -0
- package/dist/service-client/index.js.map +1 -0
- package/dist/statuses/index.d.ts +3 -1
- package/dist/statuses/index.js +17 -28
- package/dist/statuses/index.js.map +1 -1
- package/dist/supporting-files/index.d.ts +3 -1
- package/dist/supporting-files/index.js +14 -20
- package/dist/supporting-files/index.js.map +1 -1
- package/dist/tasks/index.d.ts +3 -1
- package/dist/tasks/index.js +21 -50
- package/dist/tasks/index.js.map +1 -1
- package/dist/users/dto.d.ts +48 -0
- package/dist/users/dto.js +267 -0
- package/dist/users/dto.js.map +1 -0
- package/dist/users/index.d.ts +38 -0
- package/dist/users/index.js +144 -0
- package/dist/users/index.js.map +1 -0
- package/dist/webhook-events/index.d.ts +65 -0
- package/dist/webhook-events/index.js +109 -0
- package/dist/webhook-events/index.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.UsersResource = void 0;
|
|
18
|
+
const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
19
|
+
const audit_1 = require("../audit");
|
|
20
|
+
const errors_1 = require("../errors");
|
|
21
|
+
__exportStar(require("./dto"), exports);
|
|
22
|
+
const USERS_TABLE = {
|
|
23
|
+
table: 'users',
|
|
24
|
+
entityType: enum_1.AuditTrailEntityType.USER,
|
|
25
|
+
};
|
|
26
|
+
const USER_LIST_SELECT = 'mms_id, name, surname, email, active, created_at, profile_image, last_logged_in';
|
|
27
|
+
class UsersResource {
|
|
28
|
+
supabase;
|
|
29
|
+
auth;
|
|
30
|
+
table;
|
|
31
|
+
constructor(supabase, auth, audit) {
|
|
32
|
+
this.supabase = supabase;
|
|
33
|
+
this.auth = auth;
|
|
34
|
+
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, USERS_TABLE);
|
|
35
|
+
}
|
|
36
|
+
async list(query) {
|
|
37
|
+
const page = query.page;
|
|
38
|
+
const pageSize = query.page_size;
|
|
39
|
+
const offset = (page - 1) * pageSize;
|
|
40
|
+
const trimmedSearch = query.search?.trim();
|
|
41
|
+
// `order`/`range` return `this`, so the builder stays a filter builder and the conditional
|
|
42
|
+
// filters below can be reassigned onto it without a cast.
|
|
43
|
+
let usersQuery = this.supabase
|
|
44
|
+
.from('users')
|
|
45
|
+
.select(USER_LIST_SELECT, { count: 'estimated' })
|
|
46
|
+
.eq('organization_id', this.auth.organization_id)
|
|
47
|
+
.order('created_at', { ascending: false })
|
|
48
|
+
.range(offset, offset + pageSize - 1);
|
|
49
|
+
// No filter shows everyone: inactive users must stay reachable so they can
|
|
50
|
+
// be reactivated from their detail page.
|
|
51
|
+
if (query.active_in && query.active_in.length > 0) {
|
|
52
|
+
usersQuery = usersQuery.in('active', query.active_in);
|
|
53
|
+
}
|
|
54
|
+
if (trimmedSearch) {
|
|
55
|
+
const escapedSearch = trimmedSearch.replace(/,/g, '\\,');
|
|
56
|
+
usersQuery = usersQuery.or(`name.ilike.%${escapedSearch}%,surname.ilike.%${escapedSearch}%,email.ilike.%${escapedSearch}%`);
|
|
57
|
+
}
|
|
58
|
+
const { data, error, count } = await usersQuery;
|
|
59
|
+
if (error)
|
|
60
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
61
|
+
const totalEstimated = count ?? 0;
|
|
62
|
+
const totalPages = totalEstimated > 0 ? Math.ceil(totalEstimated / pageSize) : 0;
|
|
63
|
+
return {
|
|
64
|
+
results: (data ?? []),
|
|
65
|
+
page,
|
|
66
|
+
per_page: pageSize,
|
|
67
|
+
count: totalEstimated,
|
|
68
|
+
total_pages: totalPages,
|
|
69
|
+
has_next_page: page < totalPages,
|
|
70
|
+
has_previous_page: page > 1,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async getRow(userId) {
|
|
74
|
+
const { data, error } = await this.supabase
|
|
75
|
+
.from('users')
|
|
76
|
+
.select('*')
|
|
77
|
+
.eq('mms_id', userId)
|
|
78
|
+
.eq('organization_id', this.auth.organization_id)
|
|
79
|
+
.single();
|
|
80
|
+
if (error)
|
|
81
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
82
|
+
return data;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The public profile behind a `*_by` / `manager_id` column. `user_profiles` is a
|
|
86
|
+
* `security_invoker = off` view granted to `authenticated`, so this runs on the caller's own
|
|
87
|
+
* client — it needs no service role.
|
|
88
|
+
*/
|
|
89
|
+
async identityById(userId) {
|
|
90
|
+
if (!userId)
|
|
91
|
+
return null;
|
|
92
|
+
const { data, error } = await this.supabase
|
|
93
|
+
.from('user_profiles')
|
|
94
|
+
.select('*')
|
|
95
|
+
.eq('mms_id', userId)
|
|
96
|
+
.single();
|
|
97
|
+
if (error)
|
|
98
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
/** Seats in use. Counts rows rather than fetching them. */
|
|
102
|
+
async activeCount() {
|
|
103
|
+
const { count, error } = await this.supabase
|
|
104
|
+
.from('users')
|
|
105
|
+
.select('mms_id', { count: 'exact', head: true })
|
|
106
|
+
.eq('organization_id', this.auth.organization_id)
|
|
107
|
+
.eq('active', true);
|
|
108
|
+
if (error)
|
|
109
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
110
|
+
return count ?? 0;
|
|
111
|
+
}
|
|
112
|
+
async isActive(userId) {
|
|
113
|
+
const { data, error } = await this.supabase
|
|
114
|
+
.from('users')
|
|
115
|
+
.select('active')
|
|
116
|
+
.eq('mms_id', userId)
|
|
117
|
+
.eq('organization_id', this.auth.organization_id)
|
|
118
|
+
.single();
|
|
119
|
+
if (error)
|
|
120
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
121
|
+
return data.active;
|
|
122
|
+
}
|
|
123
|
+
async update(userId, patch) {
|
|
124
|
+
await this.table.update(userId, {
|
|
125
|
+
...patch,
|
|
126
|
+
updated_at: new Date().toISOString(),
|
|
127
|
+
last_updated_by: this.auth.mms_id,
|
|
128
|
+
});
|
|
129
|
+
return this.getRow(userId);
|
|
130
|
+
}
|
|
131
|
+
/** `active` is not an archive flag, so a deactivation is recorded as a plain UPDATE. */
|
|
132
|
+
async setActive(userId, active) {
|
|
133
|
+
return this.update(userId, { active });
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Stamps who created a user. Runs on the service client during signup: the row is written by
|
|
137
|
+
* the `on_auth_user_created` trigger, so there is nothing to attribute it to until afterwards.
|
|
138
|
+
*/
|
|
139
|
+
async stampCreatedBy(userId, createdBy) {
|
|
140
|
+
return this.table.update(userId, { created_by: createdBy });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
exports.UsersResource = UsersResource;
|
|
144
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/users/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,0EAAuF;AAGvF,oCAMkB;AAElB,sCAA8C;AAG9C,wCAAsB;AAEtB,MAAM,WAAW,GAAG;IAClB,KAAK,EAAE,OAAO;IACd,UAAU,EAAE,2BAAoB,CAAC,IAAI;CAC7B,CAAC;AAEX,MAAM,gBAAgB,GACpB,iFAA0F,CAAC;AAU7F,MAAa,aAAa;IAIL;IACA;IAJF,KAAK,CAAe;IAErC,YACmB,QAA6B,EAC7B,IAAgB,EACjC,KAAoB;QAFH,aAAQ,GAAR,QAAQ,CAAqB;QAC7B,SAAI,GAAJ,IAAI,CAAY;QAGjC,IAAI,CAAC,KAAK,GAAG,IAAA,oBAAY,EAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;IAChE,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAuB;QAChC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACxB,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC;QACjC,MAAM,MAAM,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC;QACrC,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;QAE3C,2FAA2F;QAC3F,0DAA0D;QAC1D,IAAI,UAAU,GAAG,IAAI,CAAC,QAAQ;aAC3B,IAAI,CAAC,OAAO,CAAC;aACb,MAAM,CAAC,gBAAgB,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;aAChD,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC;aAChD,KAAK,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;aACzC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC;QAExC,2EAA2E;QAC3E,yCAAyC;QACzC,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClD,UAAU,GAAG,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;QACxD,CAAC;QAED,IAAI,aAAa,EAAE,CAAC;YAClB,MAAM,aAAa,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACzD,UAAU,GAAG,UAAU,CAAC,EAAE,CACxB,eAAe,aAAa,oBAAoB,aAAa,kBAAkB,aAAa,GAAG,CAChG,CAAC;QACJ,CAAC;QAED,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,MAAM,UAAU,CAAC;QAChD,IAAI,KAAK;YAAE,MAAM,IAAA,0BAAiB,EAAC,KAAK,CAAC,CAAC;QAE1C,MAAM,cAAc,GAAG,KAAK,IAAI,CAAC,CAAC;QAClC,MAAM,UAAU,GAAG,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAEjF,OAAO;YACL,OAAO,EAAE,CAAC,IAAI,IAAI,EAAE,CAA6B;YACjD,IAAI;YACJ,QAAQ,EAAE,QAAQ;YAClB,KAAK,EAAE,cAAc;YACrB,WAAW,EAAE,UAAU;YACvB,aAAa,EAAE,IAAI,GAAG,UAAU;YAChC,iBAAiB,EAAE,IAAI,GAAG,CAAC;SAC5B,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,MAAc;QACzB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ;aACxC,IAAI,CAAC,OAAO,CAAC;aACb,MAAM,CAAC,GAAG,CAAC;aACX,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC;aACpB,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC;aAChD,MAAM,EAAE,CAAC;QACZ,IAAI,KAAK;YAAE,MAAM,IAAA,0BAAiB,EAAC,KAAK,CAAC,CAAC;QAE1C,OAAO,IAA0B,CAAC;IACpC,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,YAAY,CAAC,MAAsB;QACvC,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAEzB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ;aACxC,IAAI,CAAC,eAAe,CAAC;aACrB,MAAM,CAAC,GAAG,CAAC;aACX,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC;aACpB,MAAM,EAAE,CAAC;QACZ,IAAI,KAAK;YAAE,MAAM,IAAA,0BAAiB,EAAC,KAAK,CAAC,CAAC;QAE1C,OAAO,IAA+B,CAAC;IACzC,CAAC;IAED,2DAA2D;IAC3D,KAAK,CAAC,WAAW;QACf,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ;aACzC,IAAI,CAAC,OAAO,CAAC;aACb,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;aAChD,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC;aAChD,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACtB,IAAI,KAAK;YAAE,MAAM,IAAA,0BAAiB,EAAC,KAAK,CAAC,CAAC;QAE1C,OAAO,KAAK,IAAI,CAAC,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,MAAc;QAC3B,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ;aACxC,IAAI,CAAC,OAAO,CAAC;aACb,MAAM,CAAC,QAAQ,CAAC;aAChB,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC;aACpB,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC;aAChD,MAAM,EAAE,CAAC;QACZ,IAAI,KAAK;YAAE,MAAM,IAAA,0BAAiB,EAAC,KAAK,CAAC,CAAC;QAE1C,OAAQ,IAA4B,CAAC,MAAM,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,MAAc,EAAE,KAAe;QAC1C,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE;YAC9B,GAAG,KAAK;YACR,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACpC,eAAe,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM;SAClC,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC7B,CAAC;IAED,wFAAwF;IACxF,KAAK,CAAC,SAAS,CAAC,MAAc,EAAE,MAAe;QAC7C,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IACzC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,cAAc,CAAC,MAAc,EAAE,SAAwB;QAC3D,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAC;IAC9D,CAAC;CACF;AApID,sCAoIC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { TypedSupabaseClient } from '../client';
|
|
2
|
+
import { SupabaseClientError } from '../errors';
|
|
3
|
+
/** The two provider event logs. Both have the same shape and the same claim protocol. */
|
|
4
|
+
export type WebhookEventsTable = 'paystack_webhook_events' | 'resend_webhook_events';
|
|
5
|
+
export type WebhookEventRow = {
|
|
6
|
+
mms_id: string;
|
|
7
|
+
processing_status: string;
|
|
8
|
+
};
|
|
9
|
+
/** What a claim attempt resolved to. `claimed` is the only outcome that runs a handler. */
|
|
10
|
+
export type ClaimOutcome = {
|
|
11
|
+
kind: 'claimed';
|
|
12
|
+
mms_id: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'duplicate';
|
|
15
|
+
existing: WebhookEventRow;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'vanished';
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The provider event logs — `paystack_webhook_events` and `resend_webhook_events`. Every policy
|
|
21
|
+
* on both is `false` for `authenticated`, including READ: the raw payloads are never exposed to
|
|
22
|
+
* end users, and an inbound webhook has no caller to run as.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately unaudited. These tables *are* an append-only log with their own
|
|
25
|
+
* `processing_status` lifecycle, so an audit row per delivery would log the log. The domain
|
|
26
|
+
* writes the handlers make off the back of them are audited as usual.
|
|
27
|
+
*/
|
|
28
|
+
export declare class WebhookEventsResource {
|
|
29
|
+
private readonly supabase;
|
|
30
|
+
private readonly table;
|
|
31
|
+
constructor(supabase: TypedSupabaseClient, table: WebhookEventsTable);
|
|
32
|
+
/**
|
|
33
|
+
* The dedup test. One insert, straight to `processing` — there is no useful window in which
|
|
34
|
+
* the row would sit at `received`.
|
|
35
|
+
*
|
|
36
|
+
* The unique index is partial (`where processing_status <> 'ignored_duplicate'`), so at
|
|
37
|
+
* insert time it cannot tell a failed key from a processed one. A unique violation resolves
|
|
38
|
+
* to the canonical row and the caller decides what it means; `vanished` means the row that
|
|
39
|
+
* caused the violation is already gone, and the caller should let the provider redeliver
|
|
40
|
+
* rather than guess.
|
|
41
|
+
*/
|
|
42
|
+
claim(payload: Record<string, unknown>, eventKey: string): Promise<ClaimOutcome>;
|
|
43
|
+
/** The canonical row for a key, ignoring the duplicate log rows that never collide. */
|
|
44
|
+
findByEventKey(eventKey: string): Promise<WebhookEventRow | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Reclaims a `failed` row for reprocessing, clearing the previous failure. Guarded on the
|
|
47
|
+
* row still being failed, because another redelivery may have reclaimed it first — resolves
|
|
48
|
+
* to null when it did.
|
|
49
|
+
*
|
|
50
|
+
* This is the single most important detail in the pipeline: swallowing a redelivery of a
|
|
51
|
+
* failed key loses the event forever.
|
|
52
|
+
*/
|
|
53
|
+
takeOverFailed(eventId: string, patch: Record<string, unknown>): Promise<WebhookEventRow | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Duplicate deliveries are recorded rather than dropped. The partial unique index excludes
|
|
56
|
+
* `ignored_duplicate`, so these rows never collide with each other.
|
|
57
|
+
*/
|
|
58
|
+
logIgnoredDuplicate(payload: Record<string, unknown>): Promise<void>;
|
|
59
|
+
markProcessed(eventId: string, patch: Record<string, unknown>): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Never throws: the handler's own failure is the one the caller needs to report, and a
|
|
62
|
+
* webhook the provider will redeliver is a better outcome than an error about the log.
|
|
63
|
+
*/
|
|
64
|
+
markFailed(eventId: string, patch: Record<string, unknown>, onError?: (error: SupabaseClientError) => void): Promise<void>;
|
|
65
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebhookEventsResource = void 0;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
const UNIQUE_VIOLATION = '23505';
|
|
6
|
+
/**
|
|
7
|
+
* The provider event logs — `paystack_webhook_events` and `resend_webhook_events`. Every policy
|
|
8
|
+
* on both is `false` for `authenticated`, including READ: the raw payloads are never exposed to
|
|
9
|
+
* end users, and an inbound webhook has no caller to run as.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately unaudited. These tables *are* an append-only log with their own
|
|
12
|
+
* `processing_status` lifecycle, so an audit row per delivery would log the log. The domain
|
|
13
|
+
* writes the handlers make off the back of them are audited as usual.
|
|
14
|
+
*/
|
|
15
|
+
class WebhookEventsResource {
|
|
16
|
+
supabase;
|
|
17
|
+
table;
|
|
18
|
+
constructor(supabase, table) {
|
|
19
|
+
this.supabase = supabase;
|
|
20
|
+
this.table = table;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The dedup test. One insert, straight to `processing` — there is no useful window in which
|
|
24
|
+
* the row would sit at `received`.
|
|
25
|
+
*
|
|
26
|
+
* The unique index is partial (`where processing_status <> 'ignored_duplicate'`), so at
|
|
27
|
+
* insert time it cannot tell a failed key from a processed one. A unique violation resolves
|
|
28
|
+
* to the canonical row and the caller decides what it means; `vanished` means the row that
|
|
29
|
+
* caused the violation is already gone, and the caller should let the provider redeliver
|
|
30
|
+
* rather than guess.
|
|
31
|
+
*/
|
|
32
|
+
async claim(payload, eventKey) {
|
|
33
|
+
const { data, error } = await this.supabase
|
|
34
|
+
.from(this.table)
|
|
35
|
+
.insert(payload)
|
|
36
|
+
.select('mms_id')
|
|
37
|
+
.single();
|
|
38
|
+
if (!error) {
|
|
39
|
+
return { kind: 'claimed', mms_id: data.mms_id };
|
|
40
|
+
}
|
|
41
|
+
if (error.code !== UNIQUE_VIOLATION)
|
|
42
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
43
|
+
const existing = await this.findByEventKey(eventKey);
|
|
44
|
+
return existing ? { kind: 'duplicate', existing } : { kind: 'vanished' };
|
|
45
|
+
}
|
|
46
|
+
/** The canonical row for a key, ignoring the duplicate log rows that never collide. */
|
|
47
|
+
async findByEventKey(eventKey) {
|
|
48
|
+
const { data, error } = await this.supabase
|
|
49
|
+
.from(this.table)
|
|
50
|
+
.select('mms_id, processing_status')
|
|
51
|
+
.eq('event_key', eventKey)
|
|
52
|
+
.neq('processing_status', 'ignored_duplicate')
|
|
53
|
+
.maybeSingle();
|
|
54
|
+
if (error)
|
|
55
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
56
|
+
return (data ?? null);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Reclaims a `failed` row for reprocessing, clearing the previous failure. Guarded on the
|
|
60
|
+
* row still being failed, because another redelivery may have reclaimed it first — resolves
|
|
61
|
+
* to null when it did.
|
|
62
|
+
*
|
|
63
|
+
* This is the single most important detail in the pipeline: swallowing a redelivery of a
|
|
64
|
+
* failed key loses the event forever.
|
|
65
|
+
*/
|
|
66
|
+
async takeOverFailed(eventId, patch) {
|
|
67
|
+
const { data, error } = await this.supabase
|
|
68
|
+
.from(this.table)
|
|
69
|
+
.update(patch)
|
|
70
|
+
.eq('mms_id', eventId)
|
|
71
|
+
.eq('processing_status', 'failed')
|
|
72
|
+
.select('mms_id')
|
|
73
|
+
.maybeSingle();
|
|
74
|
+
if (error)
|
|
75
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
76
|
+
return (data ?? null);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Duplicate deliveries are recorded rather than dropped. The partial unique index excludes
|
|
80
|
+
* `ignored_duplicate`, so these rows never collide with each other.
|
|
81
|
+
*/
|
|
82
|
+
async logIgnoredDuplicate(payload) {
|
|
83
|
+
const { error } = await this.supabase.from(this.table).insert(payload);
|
|
84
|
+
if (error)
|
|
85
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
86
|
+
}
|
|
87
|
+
async markProcessed(eventId, patch) {
|
|
88
|
+
const { error } = await this.supabase
|
|
89
|
+
.from(this.table)
|
|
90
|
+
.update(patch)
|
|
91
|
+
.eq('mms_id', eventId);
|
|
92
|
+
if (error)
|
|
93
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Never throws: the handler's own failure is the one the caller needs to report, and a
|
|
97
|
+
* webhook the provider will redeliver is a better outcome than an error about the log.
|
|
98
|
+
*/
|
|
99
|
+
async markFailed(eventId, patch, onError) {
|
|
100
|
+
const { error } = await this.supabase
|
|
101
|
+
.from(this.table)
|
|
102
|
+
.update(patch)
|
|
103
|
+
.eq('mms_id', eventId);
|
|
104
|
+
if (error)
|
|
105
|
+
onError?.((0, errors_1.mapPostgrestError)(error));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.WebhookEventsResource = WebhookEventsResource;
|
|
109
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webhook-events/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@managemint-solutions/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Typed Supabase data-access SDK for ManageMint Solutions",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Scott Bebington <scottbebington@gmail.com>",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"testEnvironment": "node"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@managemint-solutions/entities": "^1.
|
|
43
|
+
"@managemint-solutions/entities": "^1.2.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@nestjs/common": "^11.0.0",
|