@managemint-solutions/sdk 0.56.0 → 0.64.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 +209 -7
- package/dist/audit/document-changes.d.ts +39 -0
- package/dist/audit/document-changes.js +332 -0
- package/dist/audit/index.d.ts +50 -2
- package/dist/audit/index.js +87 -44
- package/dist/client.d.ts +6 -0
- package/dist/client.js +9 -0
- package/dist/clients/dto.d.ts +18 -3
- package/dist/clients/dto.js +72 -2
- package/dist/clients/index.js +16 -0
- package/dist/documents/content.d.ts +39 -0
- package/dist/documents/content.js +443 -0
- package/dist/documents/document-templates.d.ts +37 -0
- package/dist/documents/document-templates.js +143 -0
- package/dist/documents/documents.d.ts +154 -0
- package/dist/documents/documents.js +330 -0
- package/dist/documents/dto.d.ts +125 -0
- package/dist/documents/dto.js +530 -0
- package/dist/documents/index.d.ts +4 -0
- package/dist/documents/index.js +24 -0
- package/dist/documents/render/html.d.ts +64 -0
- package/dist/documents/render/html.js +399 -0
- package/dist/documents/render/index.d.ts +2 -0
- package/dist/documents/render/index.js +23 -0
- package/dist/documents/render/values.d.ts +86 -0
- package/dist/documents/render/values.js +142 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +27 -3
- package/dist/leads/index.js +1 -3
- package/dist/notifications/index.d.ts +6 -0
- package/dist/notifications/index.js +16 -0
- package/dist/organization-payment-methods/dto.d.ts +10 -0
- package/dist/organization-payment-methods/dto.js +56 -0
- package/dist/organization-payment-methods/index.d.ts +8 -1
- package/dist/organization-payment-methods/index.js +23 -0
- package/dist/payments/index.d.ts +67 -0
- package/dist/payments/index.js +137 -0
- package/dist/permissions/dto.d.ts +4 -0
- package/dist/permissions/dto.js +20 -8
- package/dist/service-client/index.d.ts +32 -0
- package/dist/service-client/index.js +60 -0
- package/dist/staff-client.d.ts +3 -0
- package/dist/staff-client.js +5 -0
- package/dist/supporting-files/index.js +4 -6
- package/dist/supporting-files/paths.d.ts +2 -0
- package/dist/supporting-files/paths.js +3 -1
- package/dist/validators.d.ts +6 -0
- package/dist/validators.js +14 -1
- package/dist/visibility/dto.d.ts +15 -0
- package/dist/visibility/dto.js +68 -0
- package/dist/visibility/index.d.ts +59 -0
- package/dist/visibility/index.js +165 -0
- package/package.json +6 -2
package/dist/audit/index.d.ts
CHANGED
|
@@ -34,6 +34,25 @@ export type AuditEntry = {
|
|
|
34
34
|
changes: AuditChanges;
|
|
35
35
|
relatedEntityType?: string | null;
|
|
36
36
|
relatedEntityId?: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Fields in `changes` that name the row rather than change it (a table's `identity` option):
|
|
39
|
+
* kept when an audit batch folds the entry even though they did not move, and never counted as
|
|
40
|
+
* a change of their own. Not written to the audit row.
|
|
41
|
+
*/
|
|
42
|
+
identity?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Set on a template's or document's entry (the table's `documentContent` option): its bulk
|
|
45
|
+
* columns are held whole in `changes` until the entry is written, then stored as what changed
|
|
46
|
+
* inside them (`compactDocumentChanges`) - so a batch that folds two saves still diffs the
|
|
47
|
+
* first against the last. `variables` is the row's definitions before and after, which label
|
|
48
|
+
* the values. Not written to the audit row.
|
|
49
|
+
*/
|
|
50
|
+
documentContent?: {
|
|
51
|
+
variables: {
|
|
52
|
+
old: unknown;
|
|
53
|
+
new: unknown;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
37
56
|
};
|
|
38
57
|
/** Called when the audit insert itself fails. The business write has already succeeded. */
|
|
39
58
|
export type AuditErrorHandler = (error: unknown, entries: AuditEntry[]) => void;
|
|
@@ -50,7 +69,11 @@ export declare const AUDIT_EXCLUDED_COLUMNS: ReadonlySet<string>;
|
|
|
50
69
|
* subscription codes, card metadata, tokens, an idempotency key that would let a replay be
|
|
51
70
|
* forged, an api credential's secret hash) or content the base table deliberately withholds
|
|
52
71
|
* (`authenticated` has no grant on `notifications.rendered_content`, so the rendered email must
|
|
53
|
-
* not arrive by the back door).
|
|
72
|
+
* not arrive by the back door). Two more are bulk rather than secret: a document's and a
|
|
73
|
+
* template's `blocks` and `variables` are saved whole every time, and copying both sides of that
|
|
74
|
+
* JSON into the trail on each save would dwarf everything else in it - so a created or deleted
|
|
75
|
+
* row records only the markers, and an update records what changed inside them instead
|
|
76
|
+
* (`compactDocumentChanges`, through the table's `documentContent` option).
|
|
54
77
|
* The column still shows up in the trail - that it changed is the point - with both sides
|
|
55
78
|
* replaced by `'[redacted]'`.
|
|
56
79
|
*/
|
|
@@ -62,8 +85,9 @@ export declare const isRedactedColumn: (name: string) => boolean;
|
|
|
62
85
|
* JSON (booleans stay booleans, jsonb stays an object) and are compared by serialized value, so
|
|
63
86
|
* arrays and objects are equal when their contents are. A redacted column is compared on its
|
|
64
87
|
* real values and only then redacted, so it is reported when - and only when - it truly moved.
|
|
88
|
+
* The `whole` columns keep both real values even when redacted, for the caller to compact.
|
|
65
89
|
*/
|
|
66
|
-
export declare const diffChanges: (before: AuditRow, after: AuditRow) => AuditChanges;
|
|
90
|
+
export declare const diffChanges: (before: AuditRow, after: AuditRow, whole?: readonly string[]) => AuditChanges;
|
|
67
91
|
/**
|
|
68
92
|
* A whole row as a change set - `'new'` for an insert, `'old'` for a hard delete - so the trail
|
|
69
93
|
* stays self-contained once the row is gone. Columns that hold nothing are left out; a redacted
|
|
@@ -150,6 +174,17 @@ export type AuditedTableOptions = {
|
|
|
150
174
|
* these leaves nothing to record, so it writes no audit row at all.
|
|
151
175
|
*/
|
|
152
176
|
untracked?: readonly string[];
|
|
177
|
+
/**
|
|
178
|
+
* Columns recorded on every entry - insert, update and delete - even when unchanged or normally
|
|
179
|
+
* excluded, because they say whose row it is: a share's `user_id`, without which "Role: Admin
|
|
180
|
+
* -> Read" would not say whose role changed. On an update they appear with the same old and new.
|
|
181
|
+
*/
|
|
182
|
+
identity?: readonly string[];
|
|
183
|
+
/**
|
|
184
|
+
* A template's or document's table: its `blocks`, `variables`, `variable_values` and `email`
|
|
185
|
+
* are recorded as what changed inside them, block by block and key by key, rather than whole.
|
|
186
|
+
*/
|
|
187
|
+
documentContent?: boolean;
|
|
153
188
|
};
|
|
154
189
|
/** The payload to write, or a function of the pre-read row (`togglePin`'s `!before.pinned`). */
|
|
155
190
|
type AuditedPayload = AuditRow | ((before: AuditRow) => AuditRow);
|
|
@@ -160,6 +195,13 @@ declare const buildUpdate: (supabase: SupabaseJsClient, table: string, id: strin
|
|
|
160
195
|
* guarded write, e.g. `(query) => query.eq('billing_status', expectedFrom)`.
|
|
161
196
|
*/
|
|
162
197
|
export type AuditGuard = (builder: ReturnType<typeof buildUpdate>) => ReturnType<typeof buildUpdate>;
|
|
198
|
+
/** The keyed delete every audited delete builds, before any caller-supplied guard. */
|
|
199
|
+
declare const buildDelete: (supabase: SupabaseJsClient, table: string, id: string, organizationId: string | null) => import("@supabase/postgrest-js").PostgrestFilterBuilder<any, any, any, null, string, unknown, "DELETE", false>;
|
|
200
|
+
/**
|
|
201
|
+
* Narrows an audited delete to the rows it may still remove - the delete's half of a
|
|
202
|
+
* compare-and-set, e.g. `(query) => query.eq('status', 'draft')`.
|
|
203
|
+
*/
|
|
204
|
+
export type AuditDeleteGuard = (builder: ReturnType<typeof buildDelete>) => ReturnType<typeof buildDelete>;
|
|
163
205
|
export type AuditedTable = {
|
|
164
206
|
insert(payload: AuditRow): Promise<AuditRow>;
|
|
165
207
|
insertMany(payloads: AuditRow[]): Promise<AuditRow[]>;
|
|
@@ -175,6 +217,12 @@ export type AuditedTable = {
|
|
|
175
217
|
transition(id: string, action: AuditTrailAction, payload: AuditedPayload): Promise<AuditRow>;
|
|
176
218
|
/** Hard delete. Returns the row as it was, so the caller can still act on it. */
|
|
177
219
|
remove(id: string): Promise<AuditRow>;
|
|
220
|
+
/**
|
|
221
|
+
* A hard delete that only lands while `guard` still holds, as `updateWhere` is for updates.
|
|
222
|
+
* Resolves to the row as it was, or `null` when the guard matched nothing (the row moved on
|
|
223
|
+
* first): nothing was deleted, so nothing is audited.
|
|
224
|
+
*/
|
|
225
|
+
removeWhere(id: string, guard: AuditDeleteGuard): Promise<AuditRow | null>;
|
|
178
226
|
};
|
|
179
227
|
/** How a service-client resource reaches an audited table once it knows a row's organization. */
|
|
180
228
|
export type OrganizationTableFactory = (organizationId: string) => AuditedTable;
|
package/dist/audit/index.js
CHANGED
|
@@ -20,6 +20,7 @@ const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
|
20
20
|
const errors_1 = require("../errors");
|
|
21
21
|
const tenancy_1 = require("../tenancy");
|
|
22
22
|
const transforms_1 = require("../transforms");
|
|
23
|
+
const document_changes_1 = require("./document-changes");
|
|
23
24
|
__exportStar(require("./dto"), exports);
|
|
24
25
|
/**
|
|
25
26
|
* Drops the internal `id bigint` from a row on its way out of the SDK. Every table has one
|
|
@@ -63,13 +64,19 @@ exports.AUDIT_EXCLUDED_COLUMNS = new Set([
|
|
|
63
64
|
* subscription codes, card metadata, tokens, an idempotency key that would let a replay be
|
|
64
65
|
* forged, an api credential's secret hash) or content the base table deliberately withholds
|
|
65
66
|
* (`authenticated` has no grant on `notifications.rendered_content`, so the rendered email must
|
|
66
|
-
* not arrive by the back door).
|
|
67
|
+
* not arrive by the back door). Two more are bulk rather than secret: a document's and a
|
|
68
|
+
* template's `blocks` and `variables` are saved whole every time, and copying both sides of that
|
|
69
|
+
* JSON into the trail on each save would dwarf everything else in it - so a created or deleted
|
|
70
|
+
* row records only the markers, and an update records what changed inside them instead
|
|
71
|
+
* (`compactDocumentChanges`, through the table's `documentContent` option).
|
|
67
72
|
* The column still shows up in the trail - that it changed is the point - with both sides
|
|
68
73
|
* replaced by `'[redacted]'`.
|
|
69
74
|
*/
|
|
70
75
|
exports.AUDIT_REDACTED_COLUMNS = new Set([
|
|
71
76
|
'rendered_content',
|
|
72
77
|
'template_variables',
|
|
78
|
+
'blocks',
|
|
79
|
+
'variables',
|
|
73
80
|
'idempotency_key',
|
|
74
81
|
'provider_message_id',
|
|
75
82
|
'card_expiry_date',
|
|
@@ -85,6 +92,8 @@ const isRedactedColumn = (name) => exports.AUDIT_REDACTED_COLUMNS.has(name) || n
|
|
|
85
92
|
exports.isRedactedColumn = isRedactedColumn;
|
|
86
93
|
/** What a redacted column shows instead of its value. */
|
|
87
94
|
const REDACTED = '[redacted]';
|
|
95
|
+
/** Whether `field` is one of the entry's identity columns, which name the row rather than change it. */
|
|
96
|
+
const isIdentity = (entry, field) => entry.identity?.includes(field) ?? false;
|
|
88
97
|
/** `undefined` and `null` are the same absent value as far as a row diff is concerned. */
|
|
89
98
|
const normalize = (value) => value ?? null;
|
|
90
99
|
/**
|
|
@@ -92,8 +101,9 @@ const normalize = (value) => value ?? null;
|
|
|
92
101
|
* JSON (booleans stay booleans, jsonb stays an object) and are compared by serialized value, so
|
|
93
102
|
* arrays and objects are equal when their contents are. A redacted column is compared on its
|
|
94
103
|
* real values and only then redacted, so it is reported when - and only when - it truly moved.
|
|
104
|
+
* The `whole` columns keep both real values even when redacted, for the caller to compact.
|
|
95
105
|
*/
|
|
96
|
-
const diffChanges = (before, after) => {
|
|
106
|
+
const diffChanges = (before, after, whole = []) => {
|
|
97
107
|
const changes = {};
|
|
98
108
|
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
99
109
|
if (exports.AUDIT_EXCLUDED_COLUMNS.has(key))
|
|
@@ -102,7 +112,7 @@ const diffChanges = (before, after) => {
|
|
|
102
112
|
const newValue = normalize(after[key]);
|
|
103
113
|
if (JSON.stringify(oldValue) === JSON.stringify(newValue))
|
|
104
114
|
continue;
|
|
105
|
-
changes[key] = (0, exports.isRedactedColumn)(key)
|
|
115
|
+
changes[key] = (0, exports.isRedactedColumn)(key) && !whole.includes(key)
|
|
106
116
|
? { old: REDACTED, new: REDACTED }
|
|
107
117
|
: { old: oldValue, new: newValue };
|
|
108
118
|
}
|
|
@@ -198,19 +208,24 @@ const withAuditBatch = async (work) => {
|
|
|
198
208
|
}
|
|
199
209
|
earlier.entry.relatedEntityType ??= entry.relatedEntityType;
|
|
200
210
|
earlier.entry.relatedEntityId ??= entry.relatedEntityId;
|
|
211
|
+
earlier.entry.identity ??= entry.identity;
|
|
212
|
+
if (earlier.entry.documentContent && entry.documentContent) {
|
|
213
|
+
earlier.entry.documentContent = {
|
|
214
|
+
variables: { old: earlier.entry.documentContent.variables.old, new: entry.documentContent.variables.new },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
earlier.entry.documentContent ??= entry.documentContent;
|
|
201
218
|
}
|
|
202
219
|
const byWriter = new Map();
|
|
203
220
|
for (const { entry, writer } of merged.values()) {
|
|
204
221
|
// A field that ended where it started is no change (a redacted one reads the same on both
|
|
205
|
-
// sides by design, so it stays).
|
|
222
|
+
// sides by design, so it stays; an identity field names the row, so it stays too). An
|
|
223
|
+
// update left with no change is dropped by `write`.
|
|
206
224
|
for (const [field, change] of Object.entries(entry.changes)) {
|
|
207
|
-
if (change.old !== REDACTED && JSON.stringify(change.old) === JSON.stringify(change.new)) {
|
|
225
|
+
if (!isIdentity(entry, field) && change.old !== REDACTED && JSON.stringify(change.old) === JSON.stringify(change.new)) {
|
|
208
226
|
delete entry.changes[field];
|
|
209
227
|
}
|
|
210
228
|
}
|
|
211
|
-
// Updates that cancelled out changed nothing: no row.
|
|
212
|
-
if (entry.action === enum_1.AuditTrailAction.UPDATE && Object.keys(entry.changes).length === 0)
|
|
213
|
-
continue;
|
|
214
229
|
byWriter.set(writer, [...(byWriter.get(writer) ?? []), entry]);
|
|
215
230
|
}
|
|
216
231
|
await Promise.all([...byWriter].map(([writer, entries]) => writer.write(entries)));
|
|
@@ -252,8 +267,19 @@ class AuditResource {
|
|
|
252
267
|
* writes what it held.
|
|
253
268
|
*/
|
|
254
269
|
async write(entries) {
|
|
270
|
+
// A template's or document's bulk columns become what changed inside them. An update left
|
|
271
|
+
// with nothing but its identity fields - cancelled out in a batch, or nothing printable moved
|
|
272
|
+
// (variables only reordered) - is no event.
|
|
273
|
+
const rows = entries
|
|
274
|
+
.map((item) => item.documentContent
|
|
275
|
+
? { ...item, changes: (0, document_changes_1.compactDocumentChanges)(item.changes, item.documentContent.variables) }
|
|
276
|
+
: item)
|
|
277
|
+
.filter((item) => item.action !== enum_1.AuditTrailAction.UPDATE
|
|
278
|
+
|| Object.keys(item.changes).some((field) => !isIdentity(item, field)));
|
|
279
|
+
if (rows.length === 0)
|
|
280
|
+
return;
|
|
255
281
|
try {
|
|
256
|
-
const { error } = await this.supabase.from('audit_logs').insert(
|
|
282
|
+
const { error } = await this.supabase.from('audit_logs').insert(rows.map((item) => ({
|
|
257
283
|
created_by: this.actor.mms_id,
|
|
258
284
|
organization_id: this.actor.organization_id,
|
|
259
285
|
action: item.action,
|
|
@@ -268,7 +294,7 @@ class AuditResource {
|
|
|
268
294
|
}
|
|
269
295
|
catch (error) {
|
|
270
296
|
const handle = this.onError ?? ((thrown, failed) => console.error(thrown, failed));
|
|
271
|
-
handle(error,
|
|
297
|
+
handle(error, rows);
|
|
272
298
|
}
|
|
273
299
|
}
|
|
274
300
|
/**
|
|
@@ -376,6 +402,14 @@ const buildUpdate = (supabase, table, id, payload, organizationId) => {
|
|
|
376
402
|
query = query.eq('organization_id', organizationId);
|
|
377
403
|
return query;
|
|
378
404
|
};
|
|
405
|
+
/** The keyed delete every audited delete builds, before any caller-supplied guard. */
|
|
406
|
+
const buildDelete = (supabase, table, id, organizationId) => {
|
|
407
|
+
// `eq` returns `this`, so the org filter can be reassigned onto the builder without a cast.
|
|
408
|
+
let query = supabase.from(table).delete().eq('mms_id', id);
|
|
409
|
+
if (organizationId !== null)
|
|
410
|
+
query = query.eq('organization_id', organizationId);
|
|
411
|
+
return query;
|
|
412
|
+
};
|
|
379
413
|
/**
|
|
380
414
|
* The audited write helper every resource writes through: it does the pre-read, the write, the
|
|
381
415
|
* diff and the `audit_logs` insert itself, so no feature carries audit code. The write fails
|
|
@@ -411,19 +445,32 @@ const auditedTable = (supabase, actor, audit, options) => {
|
|
|
411
445
|
const updateOf = (id, before, payload) => buildUpdate(supabase, options.table, id, typeof payload === 'function' ? payload(before) : payload, organizationFilter);
|
|
412
446
|
// The changes with the table's untracked columns taken out.
|
|
413
447
|
const tracked = (changes) => Object.fromEntries(Object.entries(changes).filter(([column]) => !options.untracked?.includes(column)));
|
|
448
|
+
// The entry for one write of a row (`before` null for an insert, `after` null for a delete): the
|
|
449
|
+
// tracked changes plus the table's identity columns, a template's or document's variable
|
|
450
|
+
// definitions to label its values, and the record the row belongs to.
|
|
451
|
+
const entryOf = (id, action, changes, before, after) => ({
|
|
452
|
+
action,
|
|
453
|
+
entityType: options.entityType,
|
|
454
|
+
entityId: id,
|
|
455
|
+
changes: {
|
|
456
|
+
...tracked(changes),
|
|
457
|
+
...Object.fromEntries((options.identity ?? []).map((column) => [column, { old: before?.[column] ?? null, new: after?.[column] ?? null }])),
|
|
458
|
+
},
|
|
459
|
+
identity: options.identity,
|
|
460
|
+
...(options.documentContent && {
|
|
461
|
+
documentContent: { variables: { old: before?.variables ?? null, new: after?.variables ?? null } },
|
|
462
|
+
}),
|
|
463
|
+
...relatedOf((before ?? after)),
|
|
464
|
+
});
|
|
414
465
|
// An update that changed nothing tracked is not an event: no row.
|
|
415
466
|
const recordWrite = async (id, action, before, after) => {
|
|
416
|
-
const changes =
|
|
417
|
-
if (action === enum_1.AuditTrailAction.UPDATE && Object.keys(changes).length === 0)
|
|
467
|
+
const changes = (0, exports.diffChanges)(before, after, options.documentContent ? document_changes_1.DOCUMENT_CONTENT_COLUMNS : []);
|
|
468
|
+
if (action === enum_1.AuditTrailAction.UPDATE && Object.keys(tracked(changes)).length === 0)
|
|
418
469
|
return;
|
|
419
|
-
await audit.record(
|
|
420
|
-
action,
|
|
421
|
-
entityType: options.entityType,
|
|
422
|
-
entityId: id,
|
|
423
|
-
changes,
|
|
424
|
-
...relatedOf(before),
|
|
425
|
-
});
|
|
470
|
+
await audit.record(entryOf(id, action, changes, before, after));
|
|
426
471
|
};
|
|
472
|
+
// A hard delete keeps the whole row, so the trail survives it.
|
|
473
|
+
const recordDelete = (id, row) => audit.record(entryOf(id, enum_1.AuditTrailAction.DELETE, (0, exports.snapshotChanges)(row, 'old'), row, null));
|
|
427
474
|
const write = async (id, action, payload) => {
|
|
428
475
|
(0, tenancy_1.requireOrganization)(actor);
|
|
429
476
|
const before = await read(id);
|
|
@@ -463,13 +510,7 @@ const auditedTable = (supabase, actor, audit, options) => {
|
|
|
463
510
|
if (error)
|
|
464
511
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
465
512
|
const row = data;
|
|
466
|
-
await audit.record(
|
|
467
|
-
action: enum_1.AuditTrailAction.INSERT,
|
|
468
|
-
entityType: options.entityType,
|
|
469
|
-
entityId: row.mms_id,
|
|
470
|
-
changes: tracked((0, exports.snapshotChanges)(row, 'new')),
|
|
471
|
-
...relatedOf(row),
|
|
472
|
-
});
|
|
513
|
+
await audit.record(entryOf(row.mms_id, enum_1.AuditTrailAction.INSERT, (0, exports.snapshotChanges)(row, 'new'), null, row));
|
|
473
514
|
return (0, exports.withoutInternalId)(row);
|
|
474
515
|
},
|
|
475
516
|
async insertMany(payloads) {
|
|
@@ -480,13 +521,7 @@ const auditedTable = (supabase, actor, audit, options) => {
|
|
|
480
521
|
if (error)
|
|
481
522
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
482
523
|
const rows = (data ?? []);
|
|
483
|
-
await audit.record(rows.map((row) => (
|
|
484
|
-
action: enum_1.AuditTrailAction.INSERT,
|
|
485
|
-
entityType: options.entityType,
|
|
486
|
-
entityId: row.mms_id,
|
|
487
|
-
changes: tracked((0, exports.snapshotChanges)(row, 'new')),
|
|
488
|
-
...relatedOf(row),
|
|
489
|
-
})));
|
|
524
|
+
await audit.record(rows.map((row) => entryOf(row.mms_id, enum_1.AuditTrailAction.INSERT, (0, exports.snapshotChanges)(row, 'new'), null, row)));
|
|
490
525
|
return rows.map(exports.withoutInternalId);
|
|
491
526
|
},
|
|
492
527
|
update(id, payload) {
|
|
@@ -501,21 +536,29 @@ const auditedTable = (supabase, actor, audit, options) => {
|
|
|
501
536
|
async remove(id) {
|
|
502
537
|
(0, tenancy_1.requireOrganization)(actor);
|
|
503
538
|
const before = await read(id);
|
|
504
|
-
|
|
505
|
-
if (organizationFilter !== null)
|
|
506
|
-
query = query.eq('organization_id', organizationFilter);
|
|
507
|
-
const { error } = await query;
|
|
539
|
+
const { error } = await buildDelete(supabase, options.table, id, organizationFilter);
|
|
508
540
|
if (error)
|
|
509
541
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
510
|
-
await
|
|
511
|
-
action: enum_1.AuditTrailAction.DELETE,
|
|
512
|
-
entityType: options.entityType,
|
|
513
|
-
entityId: id,
|
|
514
|
-
changes: tracked((0, exports.snapshotChanges)(before, 'old')),
|
|
515
|
-
...relatedOf(before),
|
|
516
|
-
});
|
|
542
|
+
await recordDelete(id, before);
|
|
517
543
|
return (0, exports.withoutInternalId)(before);
|
|
518
544
|
},
|
|
545
|
+
async removeWhere(id, guard) {
|
|
546
|
+
(0, tenancy_1.requireOrganization)(actor);
|
|
547
|
+
// The pre-read keeps a missing row the usual 406, as for every other write.
|
|
548
|
+
await read(id);
|
|
549
|
+
// `maybeSingle`, not `single`: deleting nothing is the guard doing its job, not an error.
|
|
550
|
+
const { data, error } = await guard(buildDelete(supabase, options.table, id, organizationFilter))
|
|
551
|
+
.select('*')
|
|
552
|
+
.maybeSingle();
|
|
553
|
+
if (error)
|
|
554
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
555
|
+
// The guard lost the race - the row is untouched, so there is nothing to audit.
|
|
556
|
+
if (!data)
|
|
557
|
+
return null;
|
|
558
|
+
const deleted = data;
|
|
559
|
+
await recordDelete(id, deleted);
|
|
560
|
+
return (0, exports.withoutInternalId)(deleted);
|
|
561
|
+
},
|
|
519
562
|
};
|
|
520
563
|
};
|
|
521
564
|
exports.auditedTable = auditedTable;
|
package/dist/client.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { AuditResource, type AuditErrorHandler } from './audit';
|
|
|
4
4
|
import { AdditionalDetailsResource } from './additional-details';
|
|
5
5
|
import { ClientsResource } from './clients';
|
|
6
6
|
import { ConfigsResource } from './configs';
|
|
7
|
+
import { DocumentsResource, DocumentTemplatesResource } from './documents';
|
|
7
8
|
import { NotesResource } from './notes';
|
|
8
9
|
import { NotificationsResource } from './notifications';
|
|
9
10
|
import { BillingReadsResource } from './billing';
|
|
@@ -12,6 +13,7 @@ import { LabelsResource } from './labels';
|
|
|
12
13
|
import { LeadsResource } from './leads';
|
|
13
14
|
import { LeaveAdjustmentsResource, LeaveEntitlementsResource, LeaveRequestsResource, LeaveTypesResource } from './leave';
|
|
14
15
|
import { OrganizationsResource } from './organizations';
|
|
16
|
+
import { PaymentsResource } from './payments';
|
|
15
17
|
import { PermissionsResource } from './permissions';
|
|
16
18
|
import { ProjectsResource } from './projects';
|
|
17
19
|
import { PublicHolidaysResource } from './public-holidays';
|
|
@@ -63,6 +65,10 @@ export declare class SupabaseClient {
|
|
|
63
65
|
readonly clients: ClientsResource;
|
|
64
66
|
readonly projects: ProjectsResource;
|
|
65
67
|
readonly tasks: TasksResource;
|
|
68
|
+
readonly documentTemplates: DocumentTemplatesResource;
|
|
69
|
+
readonly documents: DocumentsResource;
|
|
70
|
+
/** Read only: a member reads the payments of what they can read; the webhook writes them. */
|
|
71
|
+
readonly payments: PaymentsResource;
|
|
66
72
|
readonly leaveTypes: LeaveTypesResource;
|
|
67
73
|
readonly publicHolidays: PublicHolidaysResource;
|
|
68
74
|
readonly leaveEntitlements: LeaveEntitlementsResource;
|
package/dist/client.js
CHANGED
|
@@ -6,6 +6,7 @@ const audit_1 = require("./audit");
|
|
|
6
6
|
const additional_details_1 = require("./additional-details");
|
|
7
7
|
const clients_1 = require("./clients");
|
|
8
8
|
const configs_1 = require("./configs");
|
|
9
|
+
const documents_1 = require("./documents");
|
|
9
10
|
const notes_1 = require("./notes");
|
|
10
11
|
const notifications_1 = require("./notifications");
|
|
11
12
|
const billing_1 = require("./billing");
|
|
@@ -14,6 +15,7 @@ const labels_1 = require("./labels");
|
|
|
14
15
|
const leads_1 = require("./leads");
|
|
15
16
|
const leave_1 = require("./leave");
|
|
16
17
|
const organizations_1 = require("./organizations");
|
|
18
|
+
const payments_1 = require("./payments");
|
|
17
19
|
const permissions_1 = require("./permissions");
|
|
18
20
|
const projects_1 = require("./projects");
|
|
19
21
|
const public_holidays_1 = require("./public-holidays");
|
|
@@ -48,6 +50,10 @@ class SupabaseClient {
|
|
|
48
50
|
clients;
|
|
49
51
|
projects;
|
|
50
52
|
tasks;
|
|
53
|
+
documentTemplates;
|
|
54
|
+
documents;
|
|
55
|
+
/** Read only: a member reads the payments of what they can read; the webhook writes them. */
|
|
56
|
+
payments;
|
|
51
57
|
leaveTypes;
|
|
52
58
|
publicHolidays;
|
|
53
59
|
leaveEntitlements;
|
|
@@ -78,6 +84,9 @@ class SupabaseClient {
|
|
|
78
84
|
this.leads = new leads_1.LeadsResource(this.supabase, this.auth, this.audit, this.additionalDetails);
|
|
79
85
|
this.projects = new projects_1.ProjectsResource(this.supabase, this.auth, this.audit);
|
|
80
86
|
this.tasks = new tasks_1.TasksResource(this.supabase, this.auth, this.audit);
|
|
87
|
+
this.documentTemplates = new documents_1.DocumentTemplatesResource(this.supabase, this.auth, this.audit);
|
|
88
|
+
this.documents = new documents_1.DocumentsResource(this.supabase, this.auth, this.audit);
|
|
89
|
+
this.payments = new payments_1.PaymentsResource(this.supabase, this.auth, this.audit);
|
|
81
90
|
this.leaveTypes = new leave_1.LeaveTypesResource(this.supabase, this.auth, this.audit);
|
|
82
91
|
// Global like configs: every signed-in caller reads the same calendar, nobody writes it here.
|
|
83
92
|
this.publicHolidays = new public_holidays_1.PublicHolidaysResource(this.supabase);
|
package/dist/clients/dto.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UUID } from 'crypto';
|
|
2
|
-
import type { ClientIdDto as ClientIdDtoType, CreateClientDto as CreateClientDtoType, GetClientsDto as GetClientsDtoType, StaffGetClientsDto as StaffGetClientsDtoType, UpdateClientDto as UpdateClientDtoType } from '@managemint-solutions/entities/clients/dto';
|
|
2
|
+
import type { ClientIdDto as ClientIdDtoType, CreateClientDto as CreateClientDtoType, GetClientsDto as GetClientsDtoType, ClientBillingDetailsDto as ClientBillingDetailsDtoType, StaffGetClientsDto as StaffGetClientsDtoType, UpdateClientDto as UpdateClientDtoType } from '@managemint-solutions/entities/clients/dto';
|
|
3
3
|
export declare class GetClientsDto implements Omit<GetClientsDtoType, 'status_id_in'> {
|
|
4
4
|
readonly search?: string | null;
|
|
5
5
|
readonly page: number;
|
|
@@ -16,7 +16,21 @@ export declare class StaffGetClientsDto extends GetClientsDto implements Omit<St
|
|
|
16
16
|
export declare class ClientIdDto implements ClientIdDtoType {
|
|
17
17
|
readonly clientId: string;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* What a document bills the client by - its address in the organization's shape, its
|
|
21
|
+
* registration and VAT numbers. Each optional on a create and a save: null clears, left out keeps.
|
|
22
|
+
*/
|
|
23
|
+
declare class ClientBillingDetailsDto implements ClientBillingDetailsDtoType {
|
|
24
|
+
readonly address_line_1?: string | null;
|
|
25
|
+
readonly address_line_2?: string | null;
|
|
26
|
+
readonly city?: string | null;
|
|
27
|
+
readonly state?: string | null;
|
|
28
|
+
readonly post_code?: string | null;
|
|
29
|
+
readonly country?: string | null;
|
|
30
|
+
readonly registration_number?: string | null;
|
|
31
|
+
readonly vat_number?: string | null;
|
|
32
|
+
}
|
|
33
|
+
export declare class CreateClientDto extends ClientBillingDetailsDto implements CreateClientDtoType {
|
|
20
34
|
readonly name: string;
|
|
21
35
|
readonly email: string | null;
|
|
22
36
|
readonly phone: string | null;
|
|
@@ -24,7 +38,7 @@ export declare class CreateClientDto implements CreateClientDtoType {
|
|
|
24
38
|
readonly contract_client: boolean;
|
|
25
39
|
readonly contracted_hours: number | null;
|
|
26
40
|
}
|
|
27
|
-
export declare class UpdateClientDto implements UpdateClientDtoType {
|
|
41
|
+
export declare class UpdateClientDto extends ClientBillingDetailsDto implements UpdateClientDtoType {
|
|
28
42
|
readonly name: string;
|
|
29
43
|
readonly email: string | null;
|
|
30
44
|
readonly phone: string | null;
|
|
@@ -32,3 +46,4 @@ export declare class UpdateClientDto implements UpdateClientDtoType {
|
|
|
32
46
|
readonly contract_client: boolean;
|
|
33
47
|
readonly contracted_hours: number | null;
|
|
34
48
|
}
|
|
49
|
+
export {};
|
package/dist/clients/dto.js
CHANGED
|
@@ -97,7 +97,77 @@ __decorate([
|
|
|
97
97
|
(0, class_validator_1.IsUUID)('7', { message: 'Invalid client ID format' }),
|
|
98
98
|
__metadata("design:type", String)
|
|
99
99
|
], ClientIdDto.prototype, "clientId", void 0);
|
|
100
|
-
|
|
100
|
+
/**
|
|
101
|
+
* What a document bills the client by - its address in the organization's shape, its
|
|
102
|
+
* registration and VAT numbers. Each optional on a create and a save: null clears, left out keeps.
|
|
103
|
+
*/
|
|
104
|
+
class ClientBillingDetailsDto {
|
|
105
|
+
address_line_1;
|
|
106
|
+
address_line_2;
|
|
107
|
+
city;
|
|
108
|
+
state;
|
|
109
|
+
post_code;
|
|
110
|
+
country;
|
|
111
|
+
registration_number;
|
|
112
|
+
vat_number;
|
|
113
|
+
}
|
|
114
|
+
__decorate([
|
|
115
|
+
(0, class_validator_1.IsOptional)(),
|
|
116
|
+
(0, validators_1.IsNullable)(),
|
|
117
|
+
(0, class_validator_1.MaxLength)(200, { message: 'Address line 1 cannot exceed 200 characters' }),
|
|
118
|
+
(0, class_validator_1.IsString)({ message: 'Address line 1 must be text' }),
|
|
119
|
+
__metadata("design:type", Object)
|
|
120
|
+
], ClientBillingDetailsDto.prototype, "address_line_1", void 0);
|
|
121
|
+
__decorate([
|
|
122
|
+
(0, class_validator_1.IsOptional)(),
|
|
123
|
+
(0, validators_1.IsNullable)(),
|
|
124
|
+
(0, class_validator_1.MaxLength)(200, { message: 'Address line 2 cannot exceed 200 characters' }),
|
|
125
|
+
(0, class_validator_1.IsString)({ message: 'Address line 2 must be text' }),
|
|
126
|
+
__metadata("design:type", Object)
|
|
127
|
+
], ClientBillingDetailsDto.prototype, "address_line_2", void 0);
|
|
128
|
+
__decorate([
|
|
129
|
+
(0, class_validator_1.IsOptional)(),
|
|
130
|
+
(0, validators_1.IsNullable)(),
|
|
131
|
+
(0, class_validator_1.MaxLength)(100, { message: 'City cannot exceed 100 characters' }),
|
|
132
|
+
(0, class_validator_1.IsString)({ message: 'City must be text' }),
|
|
133
|
+
__metadata("design:type", Object)
|
|
134
|
+
], ClientBillingDetailsDto.prototype, "city", void 0);
|
|
135
|
+
__decorate([
|
|
136
|
+
(0, class_validator_1.IsOptional)(),
|
|
137
|
+
(0, validators_1.IsNullable)(),
|
|
138
|
+
(0, class_validator_1.MaxLength)(100, { message: 'State cannot exceed 100 characters' }),
|
|
139
|
+
(0, class_validator_1.IsString)({ message: 'State must be text' }),
|
|
140
|
+
__metadata("design:type", Object)
|
|
141
|
+
], ClientBillingDetailsDto.prototype, "state", void 0);
|
|
142
|
+
__decorate([
|
|
143
|
+
(0, class_validator_1.IsOptional)(),
|
|
144
|
+
(0, validators_1.IsNullable)(),
|
|
145
|
+
(0, class_validator_1.MaxLength)(20, { message: 'Post code cannot exceed 20 characters' }),
|
|
146
|
+
(0, class_validator_1.IsString)({ message: 'Post code must be text' }),
|
|
147
|
+
__metadata("design:type", Object)
|
|
148
|
+
], ClientBillingDetailsDto.prototype, "post_code", void 0);
|
|
149
|
+
__decorate([
|
|
150
|
+
(0, class_validator_1.IsOptional)(),
|
|
151
|
+
(0, validators_1.IsNullable)(),
|
|
152
|
+
(0, class_validator_1.MaxLength)(100, { message: 'Country cannot exceed 100 characters' }),
|
|
153
|
+
(0, class_validator_1.IsString)({ message: 'Country must be text' }),
|
|
154
|
+
__metadata("design:type", Object)
|
|
155
|
+
], ClientBillingDetailsDto.prototype, "country", void 0);
|
|
156
|
+
__decorate([
|
|
157
|
+
(0, class_validator_1.IsOptional)(),
|
|
158
|
+
(0, validators_1.IsNullable)(),
|
|
159
|
+
(0, class_validator_1.MaxLength)(50, { message: 'Registration number cannot exceed 50 characters' }),
|
|
160
|
+
(0, class_validator_1.IsString)({ message: 'Registration number must be text' }),
|
|
161
|
+
__metadata("design:type", Object)
|
|
162
|
+
], ClientBillingDetailsDto.prototype, "registration_number", void 0);
|
|
163
|
+
__decorate([
|
|
164
|
+
(0, class_validator_1.IsOptional)(),
|
|
165
|
+
(0, validators_1.IsNullable)(),
|
|
166
|
+
(0, class_validator_1.MaxLength)(50, { message: 'VAT number cannot exceed 50 characters' }),
|
|
167
|
+
(0, class_validator_1.IsString)({ message: 'VAT number must be text' }),
|
|
168
|
+
__metadata("design:type", Object)
|
|
169
|
+
], ClientBillingDetailsDto.prototype, "vat_number", void 0);
|
|
170
|
+
class CreateClientDto extends ClientBillingDetailsDto {
|
|
101
171
|
name;
|
|
102
172
|
email;
|
|
103
173
|
phone;
|
|
@@ -143,7 +213,7 @@ __decorate([
|
|
|
143
213
|
(0, class_validator_1.Min)(0, { message: 'Contracted hours must be at least 0' }),
|
|
144
214
|
__metadata("design:type", Object)
|
|
145
215
|
], CreateClientDto.prototype, "contracted_hours", void 0);
|
|
146
|
-
class UpdateClientDto {
|
|
216
|
+
class UpdateClientDto extends ClientBillingDetailsDto {
|
|
147
217
|
name;
|
|
148
218
|
email;
|
|
149
219
|
phone;
|
package/dist/clients/index.js
CHANGED
|
@@ -57,6 +57,14 @@ const CLIENT_DETAIL_SELECT = `
|
|
|
57
57
|
),
|
|
58
58
|
contract_client,
|
|
59
59
|
contracted_hours,
|
|
60
|
+
address_line_1,
|
|
61
|
+
address_line_2,
|
|
62
|
+
city,
|
|
63
|
+
state,
|
|
64
|
+
post_code,
|
|
65
|
+
country,
|
|
66
|
+
registration_number,
|
|
67
|
+
vat_number,
|
|
60
68
|
updated_at,
|
|
61
69
|
last_updated_by:user_profiles!last_updated_by ${transforms_1.USER_PROFILE_EMBED},
|
|
62
70
|
created_by:user_profiles!created_by ${transforms_1.USER_PROFILE_EMBED},
|
|
@@ -138,6 +146,14 @@ class ClientsResource {
|
|
|
138
146
|
status: null,
|
|
139
147
|
contract_client: false,
|
|
140
148
|
contracted_hours: 0,
|
|
149
|
+
address_line_1: null,
|
|
150
|
+
address_line_2: null,
|
|
151
|
+
city: null,
|
|
152
|
+
state: null,
|
|
153
|
+
post_code: null,
|
|
154
|
+
country: null,
|
|
155
|
+
registration_number: null,
|
|
156
|
+
vat_number: null,
|
|
141
157
|
};
|
|
142
158
|
}
|
|
143
159
|
return data;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { UpdateDocumentDto as UpdateDocumentDtoType } from '@managemint-solutions/entities/documents/dto';
|
|
2
|
+
/** The image types an image block may show - the image types supporting files accept. */
|
|
3
|
+
export declare const IMAGE_EXTENSIONS: ReadonlySet<string>;
|
|
4
|
+
/** The path `documents.createImageUploadUrl` hands out, relative to the organization's folder. */
|
|
5
|
+
export declare const IMAGE_PATH: RegExp;
|
|
6
|
+
/**
|
|
7
|
+
* A template's or document's variables: each `{ key, label, type, required, default_value }`,
|
|
8
|
+
* keys lower snake_case (1 to 40 characters), unique, never a system variable's, and a default
|
|
9
|
+
* that fits the type or null.
|
|
10
|
+
*/
|
|
11
|
+
export declare const variablesProblem: (variables: unknown) => string | null;
|
|
12
|
+
/**
|
|
13
|
+
* A template's or document's blocks: each block's shape for its type, numbers in range, and every
|
|
14
|
+
* variable it uses - `{{key}}` in plain text, a variable node in rich text, a line's quantity or
|
|
15
|
+
* price - declared in `variables` or a system variable; `{{page_number}}` and `{{page_count}}` only
|
|
16
|
+
* in the header and footer, of which a document has at most one each, anywhere in the list.
|
|
17
|
+
*/
|
|
18
|
+
export declare const blocksProblem: (blocks: unknown, variables: unknown) => string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Filled-in values by variable key: each key a declared variable (system values are the api's to
|
|
21
|
+
* fill) and each value fitting its type - text, a number, whole cents 0 or more, a YYYY-MM-DD
|
|
22
|
+
* date. A variable with no value is simply left out.
|
|
23
|
+
*/
|
|
24
|
+
export declare const variableValuesProblem: (values: unknown, variables: unknown) => string | null;
|
|
25
|
+
/** Three capital letters (ZAR), or nothing - unless a line items block needs one to print in. */
|
|
26
|
+
export declare const currencyProblem: (currency: unknown, blocks: unknown) => string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Everything `UpdateDocumentDto` checks about a document's content, as one call - for a draft the
|
|
29
|
+
* api composes itself: `POST /documents` copies a template's variables and blocks and overlays the
|
|
30
|
+
* caller's values, which no DTO saw together. Null when the draft is sound.
|
|
31
|
+
*/
|
|
32
|
+
export declare const documentProblem: (document: Pick<UpdateDocumentDtoType, "variables" | "variable_values" | "blocks" | "currency">) => string | null;
|
|
33
|
+
export declare const IsDocumentVariables: () => PropertyDecorator;
|
|
34
|
+
export declare const IsDocumentBlocks: () => PropertyDecorator;
|
|
35
|
+
export declare const IsDocumentVariableValues: () => PropertyDecorator;
|
|
36
|
+
/** On a DTO with `blocks`, also required when they hold line items; `@IsOptional()` makes it a format check only. */
|
|
37
|
+
export declare const IsDocumentCurrency: () => PropertyDecorator;
|
|
38
|
+
export declare const IsDocumentCategory: () => PropertyDecorator;
|
|
39
|
+
export declare const IsDocumentNumberPrefix: () => PropertyDecorator;
|