@managemint-solutions/sdk 0.52.0 → 0.54.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 +92 -13
- package/dist/additional-details/index.d.ts +2 -2
- package/dist/additional-details/index.js +6 -10
- package/dist/audit/dto.d.ts +3 -8
- package/dist/audit/dto.js +7 -48
- package/dist/audit/index.d.ts +71 -10
- package/dist/audit/index.js +234 -55
- package/dist/auth-client/index.d.ts +2 -0
- package/dist/auth-client/index.js +12 -0
- package/dist/auth.d.ts +21 -0
- package/dist/auth.js +47 -3
- package/dist/clients/dto.d.ts +5 -1
- package/dist/clients/dto.js +12 -1
- package/dist/clients/index.d.ts +8 -4
- package/dist/clients/index.js +11 -8
- package/dist/emulation-handshakes/index.d.ts +13 -0
- package/dist/emulation-handshakes/index.js +36 -0
- package/dist/errors.js +2 -2
- package/dist/index.d.ts +14 -10
- package/dist/index.js +23 -4
- package/dist/leads/dto.d.ts +5 -1
- package/dist/leads/dto.js +12 -1
- package/dist/leads/index.d.ts +8 -5
- package/dist/leads/index.js +23 -21
- package/dist/leave/leave-adjustments.js +1 -0
- package/dist/leave/leave-entitlements.d.ts +3 -1
- package/dist/leave/leave-entitlements.js +3 -0
- package/dist/leave/leave-requests.d.ts +3 -1
- package/dist/leave/leave-requests.js +3 -0
- package/dist/nest/index.d.ts +26 -1
- package/dist/nest/index.js +58 -1
- package/dist/notifications/index.d.ts +1 -3
- package/dist/organization-payment-methods/index.d.ts +5 -6
- package/dist/organization-payment-methods/index.js +4 -17
- package/dist/organizations/dto.d.ts +9 -1
- package/dist/organizations/dto.js +40 -1
- package/dist/organizations/index.d.ts +13 -3
- package/dist/organizations/index.js +59 -3
- package/dist/permissions/index.d.ts +6 -4
- package/dist/permissions/index.js +19 -12
- package/dist/projects/dto.d.ts +5 -1
- package/dist/projects/dto.js +12 -1
- package/dist/projects/index.d.ts +8 -4
- package/dist/projects/index.js +11 -8
- package/dist/service-client/index.d.ts +39 -1
- package/dist/service-client/index.js +89 -2
- package/dist/staff/dto.d.ts +14 -0
- package/dist/staff/dto.js +63 -0
- package/dist/staff/index.d.ts +50 -0
- package/dist/staff/index.js +139 -0
- package/dist/staff-client.d.ts +37 -0
- package/dist/staff-client.js +52 -0
- package/dist/tasks/dto.d.ts +5 -1
- package/dist/tasks/dto.js +12 -1
- package/dist/tasks/index.d.ts +15 -10
- package/dist/tasks/index.js +34 -31
- package/dist/tenancy.d.ts +13 -0
- package/dist/tenancy.js +25 -0
- package/dist/timesheets/timesheet-entries.js +1 -0
- package/dist/timesheets/timesheets.d.ts +3 -1
- package/dist/timesheets/timesheets.js +3 -0
- package/dist/user-api-credentials/dto.d.ts +12 -0
- package/dist/user-api-credentials/dto.js +56 -0
- package/dist/user-api-credentials/index.d.ts +47 -0
- package/dist/user-api-credentials/index.js +101 -0
- package/dist/users/dto.d.ts +5 -1
- package/dist/users/dto.js +13 -2
- package/dist/users/index.d.ts +7 -3
- package/dist/users/index.js +16 -29
- package/dist/validators.d.ts +2 -0
- package/dist/validators.js +6 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -210,9 +210,10 @@ A task's `task_id` (its per-organization number) is assigned by the database on
|
|
|
210
210
|
task carries its embedded `status`, `assigned_to`, `project` and `client`. The detail read also carries its `parent`,
|
|
211
211
|
`subtasks` and `related` tasks, read from `task_links` in both directions.
|
|
212
212
|
|
|
213
|
-
The client
|
|
214
|
-
by `organization_id`.
|
|
215
|
-
|
|
213
|
+
The client runs as the verified `{ mms_id, organization_id }` it is handed and scopes every
|
|
214
|
+
query by `organization_id`. Staff have a client of their own (see Staff below).
|
|
215
|
+
`supabase.supabase` is the raw `@supabase/supabase-js` client (`TypedSupabaseClient`) - the
|
|
216
|
+
escape hatch for queries not yet covered by a resource.
|
|
216
217
|
|
|
217
218
|
## Audit
|
|
218
219
|
|
|
@@ -223,20 +224,27 @@ did it, and a `changes` diff of the flat table row as real JSON (`{ field: { old
|
|
|
223
224
|
Inserts and hard deletes record a whole-row snapshot instead, so the trail survives the row.
|
|
224
225
|
Bookkeeping columns are never diffed (`id`, `mms_id`, `organization_id`, `created_at/by`,
|
|
225
226
|
`user_id`, `updated_at`, `last_updated_by`, `archived_*`, `unarchived_*`, `deleted_*`,
|
|
226
|
-
`restored_*`, `entity_type`, `entity_id`); for the tables that hang off another entity
|
|
227
|
-
|
|
228
|
-
`
|
|
227
|
+
`restored_*`, `entity_type`, `entity_id`); for the tables that hang off another entity the owner
|
|
228
|
+
becomes the audit row's `related_entity_*` instead - the `entity_type`/`entity_id` pair of notes,
|
|
229
|
+
additional details and supporting files, or the foreign key a table declares with `related`
|
|
230
|
+
(timesheet entries to their timesheet, leave adjustments to their entitlement, permissions to
|
|
231
|
+
their user).
|
|
232
|
+
|
|
233
|
+
The trail is read one record at a time. Each resource with a record page exposes its history as
|
|
234
|
+
`audit`: the record's own rows plus every row related to it, newest first.
|
|
229
235
|
|
|
230
236
|
```ts
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
});
|
|
236
|
-
// →
|
|
237
|
-
// action, entity_type and entity_id.
|
|
237
|
+
const history = await supabase.clients.audit.list(clientId, { page: 1, page_size: 10 });
|
|
238
|
+
// → MmsList<AuditTrailEntity>. Also on leads, projects, tasks, users, organizations,
|
|
239
|
+
// timesheets, leaveRequests and leaveEntitlements.
|
|
240
|
+
|
|
241
|
+
const names = await supabase.audit.names({ statuses: [statusId], users: [userId] });
|
|
242
|
+
// → { [mms_id]: 'In Progress', ... } - readable names for the ids a page of history holds.
|
|
238
243
|
```
|
|
239
244
|
|
|
245
|
+
Rows older than a year are deleted by the api's daily retention cron, through the service
|
|
246
|
+
client's `purgeAuditLogsBefore(cutoff)`.
|
|
247
|
+
|
|
240
248
|
An audit insert **never fails a business write**: if it fails, the error goes to `onAuditError`
|
|
241
249
|
(default `console.error`) and the write still returns normally.
|
|
242
250
|
|
|
@@ -337,6 +345,77 @@ export class AuthController {
|
|
|
337
345
|
}
|
|
338
346
|
```
|
|
339
347
|
|
|
348
|
+
## Staff
|
|
349
|
+
|
|
350
|
+
ManageMint Solutions employees sign in to the admin api with a token whose role is `staff`. Their
|
|
351
|
+
client is a `SupabaseStaffClient`, which carries only what the admin api reads: `clients`, `leads`,
|
|
352
|
+
`projects`, `tasks`, `users`, `organizations`, `staff`, `staffPermissions` and
|
|
353
|
+
`userApiCredentials`. A resource not on it is not available to staff.
|
|
354
|
+
|
|
355
|
+
- **Reads span every organization.** The shared resources drop their organization filter and the
|
|
356
|
+
`staff` role's policies decide what comes back. The `clients`, `leads`, `projects`, `tasks` and
|
|
357
|
+
`users` lists embed each row's `organization` (`{ mms_id, name }`) and take an `organization_id`
|
|
358
|
+
filter through the staff query DTOs (`StaffGetClientsDto`, ...); a tenant's request and response
|
|
359
|
+
are unchanged. `organizations.list` pages every organization (`search` over name, trading
|
|
360
|
+
name, email).
|
|
361
|
+
- **Every write is refused** with a `403 Permission Error` before any statement runs, as is the
|
|
362
|
+
tenant-only `organizations.get()`.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
import { StaffGetClientsDto, type SupabaseStaffClient as StaffClient } from '@managemint-solutions/sdk';
|
|
366
|
+
import { attachSupabaseStaffRequestContext, SupabaseStaffClient } from '@managemint-solutions/sdk/nest';
|
|
367
|
+
|
|
368
|
+
// In the admin api's guard: verifies the token (role `staff`, an active `staff` row) and
|
|
369
|
+
// stores the context under its own key, `request.supabaseStaff`.
|
|
370
|
+
const { staff, client } = await attachSupabaseStaffRequestContext(request);
|
|
371
|
+
const flags = await client.staffPermissions.flagsFor(staff.mms_id, 'read_clients'); // is_admin + the flag
|
|
372
|
+
|
|
373
|
+
// In a controller:
|
|
374
|
+
list(@SupabaseStaffClient() supabase: StaffClient, @Query() query: StaffGetClientsDto) {
|
|
375
|
+
return supabase.clients.list(query); // every organization's, each row with its organization
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`verifyAccessToken` refuses any token whose role is not `authenticated`, so a staff token never
|
|
380
|
+
reaches a tenant route.
|
|
381
|
+
|
|
382
|
+
Staff and their flags are written on the service client only - staff hold SELECT alone:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
await service.staff.preRegister({ email, name, created_by: staffId }); // mms_id null until linked
|
|
386
|
+
try {
|
|
387
|
+
await service.createStaffAuthUser({ email, password }); // role staff: the trigger links the row
|
|
388
|
+
} catch (error) {
|
|
389
|
+
await service.staff.removePending(email);
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
await service.staff.setActive(staffId, false, byStaffId); // stamps deactivated_at/by
|
|
393
|
+
await service.setAuthUserBanned(staffId, true); // and ends their sign-ins
|
|
394
|
+
await service.staffPermissions.updateForStaff(staffId, patch, byStaffId); // UpdateStaffPermissionsDto
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### User api credentials and emulation
|
|
398
|
+
|
|
399
|
+
A credential is a client id and secret staff issue for one tenant user. Exchanging it mints that
|
|
400
|
+
user's own token, which the custom access token hook marks `emulated: true` and gives a one-hour
|
|
401
|
+
`exp`; `verifyAccessToken` returns it as `emulated: true` (`false` for a user's own session).
|
|
402
|
+
|
|
403
|
+
The mint itself lives in the admin api. The SDK calls it is made of:
|
|
404
|
+
|
|
405
|
+
- `service.userApiCredentials.findByClientId(clientId)` - the credential with its `secret_hash`,
|
|
406
|
+
or null.
|
|
407
|
+
- `service.emulationHandshakes.open(userId, credentialId)` - a 30-second handshake; returns its id.
|
|
408
|
+
- `service.mintMagicLink(email)`, then `auth.exchangeMagicLinkToken({ token_hash })` - the sign-in
|
|
409
|
+
the hook spends the handshake on.
|
|
410
|
+
- `service.emulationHandshakes.close(id)` - in a `finally`, whatever happened above.
|
|
411
|
+
- `auth.signOut(accessToken)` afterwards, so only the access token outlives the mint.
|
|
412
|
+
- `service.userApiCredentials.markUsed(credentialId)` - stamps `last_used_at`, unaudited.
|
|
413
|
+
|
|
414
|
+
`service.userApiCredentials.create` and `revoke` (null when already revoked) are audited against
|
|
415
|
+
the credential's organization as the system and show in the user's history; `secret_hash` is a
|
|
416
|
+
redacted column. Staff list a user's credentials on their own client
|
|
417
|
+
(`supabase.userApiCredentials.listForUser`), through a column grant that never includes the hash.
|
|
418
|
+
|
|
340
419
|
## Billing and invoices
|
|
341
420
|
|
|
342
421
|
Billing is split by who may write. On the caller's client, `supabase.billing` and
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AdditionalDetailSummary, AdditionalDetailsEntity } from '@managemint-solutions/entities/additional-details';
|
|
2
2
|
import type { AddAdditionalDetailsDto, UpdateAdditionalDetailsDto } from '@managemint-solutions/entities/additional-details/dto';
|
|
3
3
|
import { AdditionalDetailsEntityTypes } from '@managemint-solutions/entities/additional-details/enum';
|
|
4
|
-
import type {
|
|
4
|
+
import type { ClientContext } from '../auth';
|
|
5
5
|
import { type AuditResource } from '../audit';
|
|
6
6
|
import type { TypedSupabaseClient } from '../client';
|
|
7
7
|
export * from './dto';
|
|
@@ -10,7 +10,7 @@ export declare class AdditionalDetailsResource {
|
|
|
10
10
|
private readonly supabase;
|
|
11
11
|
private readonly auth;
|
|
12
12
|
private readonly table;
|
|
13
|
-
constructor(supabase: TypedSupabaseClient, auth:
|
|
13
|
+
constructor(supabase: TypedSupabaseClient, auth: ClientContext, audit: AuditResource);
|
|
14
14
|
getById(additionalDetailsId: string): Promise<AdditionalDetailsEntity>;
|
|
15
15
|
list(entity: AdditionalDetailsEntityTypes, entityId: string): Promise<AdditionalDetailSummary[]>;
|
|
16
16
|
create(entity: AdditionalDetailsEntityTypes, entityId: string, input: AddAdditionalDetailsDto): Promise<AdditionalDetailsEntity>;
|
|
@@ -19,6 +19,7 @@ const enum_1 = require("@managemint-solutions/entities/additional-details/enum")
|
|
|
19
19
|
const enum_2 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
20
20
|
const audit_1 = require("../audit");
|
|
21
21
|
const errors_1 = require("../errors");
|
|
22
|
+
const tenancy_1 = require("../tenancy");
|
|
22
23
|
const won_1 = require("../leads/won");
|
|
23
24
|
const values_1 = require("./values");
|
|
24
25
|
__exportStar(require("./dto"), exports);
|
|
@@ -47,10 +48,7 @@ class AdditionalDetailsResource {
|
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
async getById(additionalDetailsId) {
|
|
50
|
-
const { data, error } = await this.supabase
|
|
51
|
-
.from('additional_details')
|
|
52
|
-
.select(ADDITIONAL_DETAILS_SELECT)
|
|
53
|
-
.eq('organization_id', this.auth.organization_id)
|
|
51
|
+
const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('additional_details').select(ADDITIONAL_DETAILS_SELECT), this.auth)
|
|
54
52
|
.eq('mms_id', additionalDetailsId)
|
|
55
53
|
.single();
|
|
56
54
|
if (error)
|
|
@@ -58,10 +56,7 @@ class AdditionalDetailsResource {
|
|
|
58
56
|
return withCastValue(data);
|
|
59
57
|
}
|
|
60
58
|
async list(entity, entityId) {
|
|
61
|
-
const { data, error } = await this.supabase
|
|
62
|
-
.from('additional_details')
|
|
63
|
-
.select(ADDITIONAL_DETAILS_SUMMARY_SELECT)
|
|
64
|
-
.eq('organization_id', this.auth.organization_id)
|
|
59
|
+
const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('additional_details').select(ADDITIONAL_DETAILS_SUMMARY_SELECT), this.auth)
|
|
65
60
|
.eq('entity_type', entity)
|
|
66
61
|
.eq('entity_id', entityId)
|
|
67
62
|
.order('key', { ascending: false });
|
|
@@ -70,11 +65,12 @@ class AdditionalDetailsResource {
|
|
|
70
65
|
return data.map(withCastValue);
|
|
71
66
|
}
|
|
72
67
|
async create(entity, entityId, input) {
|
|
68
|
+
const organizationId = (0, tenancy_1.requireOrganization)(this.auth);
|
|
73
69
|
if (entity === enum_1.AdditionalDetailsEntityTypes.LEAD) {
|
|
74
|
-
await (0, won_1.refuseWonLead)(this.supabase,
|
|
70
|
+
await (0, won_1.refuseWonLead)(this.supabase, organizationId, entityId);
|
|
75
71
|
}
|
|
76
72
|
const row = await this.table.insert({
|
|
77
|
-
organization_id:
|
|
73
|
+
organization_id: organizationId,
|
|
78
74
|
entity_type: entity,
|
|
79
75
|
entity_id: entityId,
|
|
80
76
|
key: input.key,
|
package/dist/audit/dto.d.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AuditTrailEntityType } from '@managemint-solutions/entities/audit-trail/enum';
|
|
2
2
|
import type { GetAuditTrailDto as GetAuditTrailDtoType } from '@managemint-solutions/entities/audit-trail/dto';
|
|
3
3
|
export declare class GetAuditTrailDto implements GetAuditTrailDtoType {
|
|
4
|
-
readonly search?: string | null;
|
|
5
4
|
readonly page: number;
|
|
6
5
|
readonly page_size: number;
|
|
7
|
-
readonly
|
|
8
|
-
readonly
|
|
9
|
-
readonly created_by: string | null;
|
|
10
|
-
readonly action: AuditTrailAction | null;
|
|
11
|
-
readonly entity_type: AuditTrailEntityType | null;
|
|
12
|
-
readonly entity_id: string | null;
|
|
6
|
+
readonly search?: string | null;
|
|
7
|
+
readonly entity_type?: AuditTrailEntityType | null;
|
|
13
8
|
}
|
package/dist/audit/dto.js
CHANGED
|
@@ -15,27 +15,15 @@ const class_transformer_1 = require("class-transformer");
|
|
|
15
15
|
const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
16
16
|
const validators_1 = require("../validators");
|
|
17
17
|
// class-validator version of the shared GetAuditTrailDto type from
|
|
18
|
-
// @managemint-solutions/entities, so the wire contract cannot drift.
|
|
18
|
+
// @managemint-solutions/entities, so the wire contract cannot drift. The record itself is the
|
|
19
|
+
// filter; these page, search and narrow its history.
|
|
19
20
|
class GetAuditTrailDto {
|
|
20
|
-
search;
|
|
21
21
|
page = 1;
|
|
22
22
|
page_size = 10;
|
|
23
|
-
|
|
24
|
-
created_before;
|
|
25
|
-
created_by;
|
|
26
|
-
action;
|
|
23
|
+
search;
|
|
27
24
|
entity_type;
|
|
28
|
-
entity_id;
|
|
29
25
|
}
|
|
30
26
|
exports.GetAuditTrailDto = GetAuditTrailDto;
|
|
31
|
-
__decorate([
|
|
32
|
-
(0, class_validator_1.IsOptional)(),
|
|
33
|
-
(0, validators_1.IsNullable)(),
|
|
34
|
-
(0, class_validator_1.IsString)({ message: 'Search query must be a string' }),
|
|
35
|
-
(0, class_validator_1.MinLength)(1, { message: 'Search query cannot be empty' }),
|
|
36
|
-
(0, class_validator_1.MaxLength)(255, { message: 'Search query cannot exceed 255 characters' }),
|
|
37
|
-
__metadata("design:type", Object)
|
|
38
|
-
], GetAuditTrailDto.prototype, "search", void 0);
|
|
39
27
|
__decorate([
|
|
40
28
|
(0, class_validator_1.IsOptional)(),
|
|
41
29
|
(0, class_transformer_1.Transform)(({ value }) => value ? Number(value) : 1, { toClassOnly: true }),
|
|
@@ -54,43 +42,14 @@ __decorate([
|
|
|
54
42
|
__decorate([
|
|
55
43
|
(0, class_validator_1.IsOptional)(),
|
|
56
44
|
(0, validators_1.IsNullable)(),
|
|
57
|
-
(0, class_validator_1.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
__decorate([
|
|
61
|
-
(0, class_validator_1.IsOptional)(),
|
|
62
|
-
(0, validators_1.IsNullable)(),
|
|
63
|
-
(0, class_validator_1.IsDateString)({}, { message: 'End date must be a valid ISO 8601 date string' }),
|
|
64
|
-
__metadata("design:type", Object)
|
|
65
|
-
], GetAuditTrailDto.prototype, "created_before", void 0);
|
|
66
|
-
__decorate([
|
|
67
|
-
(0, class_validator_1.IsOptional)(),
|
|
68
|
-
(0, validators_1.IsNullable)()
|
|
69
|
-
// Still v4: `audit_logs.created_by` is a `users.mms_id`, which is the GoTrue auth uid the
|
|
70
|
-
// signup trigger copies out of `auth.users.id` - the table's uuidv7 default never fires.
|
|
71
|
-
,
|
|
72
|
-
(0, class_validator_1.IsUUID)('4', { message: 'Created by must be a valid UUID v4' }),
|
|
73
|
-
__metadata("design:type", Object)
|
|
74
|
-
], GetAuditTrailDto.prototype, "created_by", void 0);
|
|
75
|
-
__decorate([
|
|
76
|
-
(0, class_validator_1.IsOptional)(),
|
|
77
|
-
(0, validators_1.IsNullable)(),
|
|
78
|
-
(0, class_validator_1.IsEnum)(enum_1.AuditTrailAction, { message: `Action must be one of: ${Object.values(enum_1.AuditTrailAction).join(', ')}` }),
|
|
45
|
+
(0, class_validator_1.IsString)({ message: 'Search query must be a string' }),
|
|
46
|
+
(0, class_validator_1.MinLength)(1, { message: 'Search query cannot be empty' }),
|
|
47
|
+
(0, class_validator_1.MaxLength)(100, { message: 'Search query cannot exceed 100 characters' }),
|
|
79
48
|
__metadata("design:type", Object)
|
|
80
|
-
], GetAuditTrailDto.prototype, "
|
|
49
|
+
], GetAuditTrailDto.prototype, "search", void 0);
|
|
81
50
|
__decorate([
|
|
82
51
|
(0, class_validator_1.IsOptional)(),
|
|
83
52
|
(0, validators_1.IsNullable)(),
|
|
84
53
|
(0, class_validator_1.IsEnum)(enum_1.AuditTrailEntityType, { message: `Entity type must be one of: ${Object.values(enum_1.AuditTrailEntityType).join(', ')}` }),
|
|
85
54
|
__metadata("design:type", Object)
|
|
86
55
|
], GetAuditTrailDto.prototype, "entity_type", void 0);
|
|
87
|
-
__decorate([
|
|
88
|
-
(0, class_validator_1.IsOptional)(),
|
|
89
|
-
(0, validators_1.IsNullable)()
|
|
90
|
-
// Both versions, deliberately: `entity_id` is polymorphic over `entity_type`, so it is a v7
|
|
91
|
-
// `mms_id` for every entity type except `users`, whose `mms_id` is the GoTrue auth uid (see
|
|
92
|
-
// UserIdDto). Pinning this to one version would make one of those two unfilterable.
|
|
93
|
-
,
|
|
94
|
-
(0, class_validator_1.IsUUID)(['4', '7'], { message: 'Entity ID must be a valid UUID' }),
|
|
95
|
-
__metadata("design:type", Object)
|
|
96
|
-
], GetAuditTrailDto.prototype, "entity_id", void 0);
|
package/dist/audit/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { SupabaseClient as SupabaseJsClient } from '@supabase/supabase-js';
|
|
|
2
2
|
import type { AuditTrailEntity } from '@managemint-solutions/entities/audit-trail';
|
|
3
3
|
import { AuditTrailAction, type AuditTrailEntityType } from '@managemint-solutions/entities/audit-trail/enum';
|
|
4
4
|
import type { MmsList } from '@managemint-solutions/entities/common';
|
|
5
|
+
import type { StaffContext } from '../auth';
|
|
5
6
|
import type { GetAuditTrailDto } from './dto';
|
|
6
7
|
export * from './dto';
|
|
7
8
|
/**
|
|
@@ -12,6 +13,8 @@ export type AuditActor = {
|
|
|
12
13
|
mms_id: string | null;
|
|
13
14
|
organization_id: string;
|
|
14
15
|
};
|
|
16
|
+
/** Who a resource runs as: a write's actor, or staff, who read across organizations and write nothing. */
|
|
17
|
+
export type ResourceActor = AuditActor | StaffContext;
|
|
15
18
|
/** A flat database row - always `select('*')`, never an entity shape with embedded joins. */
|
|
16
19
|
export type AuditRow = Record<string, unknown>;
|
|
17
20
|
/**
|
|
@@ -45,8 +48,9 @@ export declare const AUDIT_EXCLUDED_COLUMNS: ReadonlySet<string>;
|
|
|
45
48
|
* `read_logs`, so a value copied into `changes` is a value published to the whole organization -
|
|
46
49
|
* and these are either provider credentials and identifiers (the `paystack_*` customer and
|
|
47
50
|
* subscription codes, card metadata, tokens, an idempotency key that would let a replay be
|
|
48
|
-
* forged) or content the base table deliberately withholds
|
|
49
|
-
* `notifications.rendered_content`, so the rendered email must
|
|
51
|
+
* forged, an api credential's secret hash) or content the base table deliberately withholds
|
|
52
|
+
* (`authenticated` has no grant on `notifications.rendered_content`, so the rendered email must
|
|
53
|
+
* not arrive by the back door).
|
|
50
54
|
* The column still shows up in the trail - that it changed is the point - with both sides
|
|
51
55
|
* replaced by `'[redacted]'`.
|
|
52
56
|
*/
|
|
@@ -66,22 +70,61 @@ export declare const diffChanges: (before: AuditRow, after: AuditRow) => AuditCh
|
|
|
66
70
|
* column that holds something is present, as `'[redacted]'`.
|
|
67
71
|
*/
|
|
68
72
|
export declare const snapshotChanges: (row: AuditRow, side: "new" | "old") => AuditChanges;
|
|
73
|
+
/** The tables a history's foreign-key values can be named from. */
|
|
74
|
+
export type AuditNameTable = 'users' | 'statuses' | 'clients' | 'projects' | 'tasks' | 'leave_types';
|
|
69
75
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
76
|
+
* Runs `work` as one event: every audit entry recorded inside it - by any client or service
|
|
77
|
+
* scope - is held, then written once it settles (or throws, since its writes still happened)
|
|
78
|
+
* as **one row per record**. The api wraps each request in it, so a save that writes a task and
|
|
79
|
+
* its labels, or a webhook that moves an organization twice, logs one row, whatever code wrote it.
|
|
80
|
+
*/
|
|
81
|
+
export declare const withAuditBatch: <T>(work: () => Promise<T>) => Promise<T>;
|
|
82
|
+
/**
|
|
83
|
+
* The `audit_logs` writer. Every audited write calls `record` for itself - no feature ever
|
|
84
|
+
* writes an audit line by hand. Reads are per record, through `EntityAuditTrail`.
|
|
72
85
|
*/
|
|
73
86
|
export declare class AuditResource {
|
|
74
87
|
private readonly supabase;
|
|
75
88
|
private readonly actor;
|
|
76
89
|
private readonly onError?;
|
|
77
|
-
constructor(supabase: SupabaseJsClient, actor:
|
|
90
|
+
constructor(supabase: SupabaseJsClient, actor: ResourceActor, onError?: AuditErrorHandler | undefined);
|
|
78
91
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* (default `console.error`) instead.
|
|
92
|
+
* Records the entries: held by the current event's batch when there is one (`withAuditBatch`),
|
|
93
|
+
* written straight away when there is not.
|
|
82
94
|
*/
|
|
83
95
|
record(entry: AuditEntry | AuditEntry[]): Promise<void>;
|
|
84
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Inserts the audit row(s) - one insert, however many entries. **Never throws**: a logging
|
|
98
|
+
* problem must not fail the business write, so a failure goes to `onAuditError`
|
|
99
|
+
* (default `console.error`) instead. Called by `record`, and by `withAuditBatch` when it
|
|
100
|
+
* writes what it held.
|
|
101
|
+
*/
|
|
102
|
+
write(entries: AuditEntry[]): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Readable names for the ids a page of history holds (`status_id`, `assigned_to_id`, ...),
|
|
105
|
+
* keyed by id: one query per table asked about. An id with no row - the record was hard
|
|
106
|
+
* deleted - is simply absent, and the caller says so.
|
|
107
|
+
*/
|
|
108
|
+
names(ids: Partial<Record<AuditNameTable, string[]>>): Promise<Record<string, string>>;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* One record's history: the rows about the record itself and every row that hangs off it (a
|
|
112
|
+
* note, a file, a timesheet entry, a user's permissions), newest first. Each resource with a
|
|
113
|
+
* record page exposes one as `audit`, so a client's history is `supabase.clients.audit.list(id)`.
|
|
114
|
+
*/
|
|
115
|
+
export declare class EntityAuditTrail {
|
|
116
|
+
private readonly supabase;
|
|
117
|
+
private readonly actor;
|
|
118
|
+
private readonly entityType;
|
|
119
|
+
constructor(supabase: SupabaseJsClient, actor: ResourceActor, entityType: AuditTrailEntityType);
|
|
120
|
+
/**
|
|
121
|
+
* A page of the history. `entity_type` keeps the entries about one kind of record (the record
|
|
122
|
+
* itself, or its notes ...). `search` matches what a change recorded - the stored text, field
|
|
123
|
+
* names included - and, because a foreign key is stored as an id, also any change holding the
|
|
124
|
+
* id of a person, status, client, project, task or leave type whose name matches, and any
|
|
125
|
+
* change made by a person whose name matches.
|
|
126
|
+
*/
|
|
127
|
+
list(entityId: string, query: GetAuditTrailDto): Promise<MmsList<AuditTrailEntity>>;
|
|
85
128
|
}
|
|
86
129
|
export type AuditedTableOptions = {
|
|
87
130
|
table: string;
|
|
@@ -91,6 +134,22 @@ export type AuditedTableOptions = {
|
|
|
91
134
|
* table itself, whose own `mms_id` is the organization.
|
|
92
135
|
*/
|
|
93
136
|
scopeByOrganization?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* The record a row belongs to, for a table that hangs off one other table by a foreign key
|
|
139
|
+
* (`timesheet_entries.timesheet_id`). Its writes are recorded as related to that record, so
|
|
140
|
+
* they show in the record's history. The polymorphic tables need no option: their own
|
|
141
|
+
* `entity_type`/`entity_id` pair says whose they are.
|
|
142
|
+
*/
|
|
143
|
+
related?: {
|
|
144
|
+
entityType: AuditTrailEntityType;
|
|
145
|
+
column: string;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Columns this table's trail never records - machinery whose history is kept elsewhere (an
|
|
149
|
+
* organization's subscription state lives in the billing ledger). A write that changes only
|
|
150
|
+
* these leaves nothing to record, so it writes no audit row at all.
|
|
151
|
+
*/
|
|
152
|
+
untracked?: readonly string[];
|
|
94
153
|
};
|
|
95
154
|
/** The payload to write, or a function of the pre-read row (`togglePin`'s `!before.pinned`). */
|
|
96
155
|
type AuditedPayload = AuditRow | ((before: AuditRow) => AuditRow);
|
|
@@ -117,9 +176,11 @@ export type AuditedTable = {
|
|
|
117
176
|
/** Hard delete. Returns the row as it was, so the caller can still act on it. */
|
|
118
177
|
remove(id: string): Promise<AuditRow>;
|
|
119
178
|
};
|
|
179
|
+
/** How a service-client resource reaches an audited table once it knows a row's organization. */
|
|
180
|
+
export type OrganizationTableFactory = (organizationId: string) => AuditedTable;
|
|
120
181
|
/**
|
|
121
182
|
* The audited write helper every resource writes through: it does the pre-read, the write, the
|
|
122
183
|
* diff and the `audit_logs` insert itself, so no feature carries audit code. The write fails
|
|
123
184
|
* loudly (`SupabaseClientError`); only the audit insert is swallowed.
|
|
124
185
|
*/
|
|
125
|
-
export declare const auditedTable: (supabase: SupabaseJsClient, actor:
|
|
186
|
+
export declare const auditedTable: (supabase: SupabaseJsClient, actor: ResourceActor, audit: AuditResource, options: AuditedTableOptions) => AuditedTable;
|