@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.
Files changed (72) hide show
  1. package/README.md +92 -13
  2. package/dist/additional-details/index.d.ts +2 -2
  3. package/dist/additional-details/index.js +6 -10
  4. package/dist/audit/dto.d.ts +3 -8
  5. package/dist/audit/dto.js +7 -48
  6. package/dist/audit/index.d.ts +71 -10
  7. package/dist/audit/index.js +234 -55
  8. package/dist/auth-client/index.d.ts +2 -0
  9. package/dist/auth-client/index.js +12 -0
  10. package/dist/auth.d.ts +21 -0
  11. package/dist/auth.js +47 -3
  12. package/dist/clients/dto.d.ts +5 -1
  13. package/dist/clients/dto.js +12 -1
  14. package/dist/clients/index.d.ts +8 -4
  15. package/dist/clients/index.js +11 -8
  16. package/dist/emulation-handshakes/index.d.ts +13 -0
  17. package/dist/emulation-handshakes/index.js +36 -0
  18. package/dist/errors.js +2 -2
  19. package/dist/index.d.ts +14 -10
  20. package/dist/index.js +23 -4
  21. package/dist/leads/dto.d.ts +5 -1
  22. package/dist/leads/dto.js +12 -1
  23. package/dist/leads/index.d.ts +8 -5
  24. package/dist/leads/index.js +23 -21
  25. package/dist/leave/leave-adjustments.js +1 -0
  26. package/dist/leave/leave-entitlements.d.ts +3 -1
  27. package/dist/leave/leave-entitlements.js +3 -0
  28. package/dist/leave/leave-requests.d.ts +3 -1
  29. package/dist/leave/leave-requests.js +3 -0
  30. package/dist/nest/index.d.ts +26 -1
  31. package/dist/nest/index.js +58 -1
  32. package/dist/notifications/index.d.ts +1 -3
  33. package/dist/organization-payment-methods/index.d.ts +5 -6
  34. package/dist/organization-payment-methods/index.js +4 -17
  35. package/dist/organizations/dto.d.ts +9 -1
  36. package/dist/organizations/dto.js +40 -1
  37. package/dist/organizations/index.d.ts +13 -3
  38. package/dist/organizations/index.js +59 -3
  39. package/dist/permissions/index.d.ts +6 -4
  40. package/dist/permissions/index.js +19 -12
  41. package/dist/projects/dto.d.ts +5 -1
  42. package/dist/projects/dto.js +12 -1
  43. package/dist/projects/index.d.ts +8 -4
  44. package/dist/projects/index.js +11 -8
  45. package/dist/service-client/index.d.ts +39 -1
  46. package/dist/service-client/index.js +89 -2
  47. package/dist/staff/dto.d.ts +14 -0
  48. package/dist/staff/dto.js +63 -0
  49. package/dist/staff/index.d.ts +50 -0
  50. package/dist/staff/index.js +139 -0
  51. package/dist/staff-client.d.ts +37 -0
  52. package/dist/staff-client.js +52 -0
  53. package/dist/tasks/dto.d.ts +5 -1
  54. package/dist/tasks/dto.js +12 -1
  55. package/dist/tasks/index.d.ts +15 -10
  56. package/dist/tasks/index.js +34 -31
  57. package/dist/tenancy.d.ts +13 -0
  58. package/dist/tenancy.js +25 -0
  59. package/dist/timesheets/timesheet-entries.js +1 -0
  60. package/dist/timesheets/timesheets.d.ts +3 -1
  61. package/dist/timesheets/timesheets.js +3 -0
  62. package/dist/user-api-credentials/dto.d.ts +12 -0
  63. package/dist/user-api-credentials/dto.js +56 -0
  64. package/dist/user-api-credentials/index.d.ts +47 -0
  65. package/dist/user-api-credentials/index.js +101 -0
  66. package/dist/users/dto.d.ts +5 -1
  67. package/dist/users/dto.js +13 -2
  68. package/dist/users/index.d.ts +7 -3
  69. package/dist/users/index.js +16 -29
  70. package/dist/validators.d.ts +2 -0
  71. package/dist/validators.js +6 -1
  72. 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 decodes `{ mms_id, organization_id }` from the JWT itself and scopes every query
214
- by `organization_id`. `supabase.supabase` is the raw `@supabase/supabase-js` client
215
- (`TypedSupabaseClient`) - the escape hatch for queries not yet covered by a resource.
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 (notes,
227
- additional details, supporting files) the `entity_type`/`entity_id` pair becomes the audit row's
228
- `related_entity_*` instead.
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 trail = await supabase.audit.list({
232
- page: 1,
233
- page_size: 10,
234
- entity_type: AuditTrailEntityType.CLIENTS,
235
- });
236
- // → MmsList<AuditTrailEntity>, newest first; filter by created_after/before, created_by,
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 { AuthContext } from '../auth';
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: AuthContext, audit: AuditResource);
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, this.auth.organization_id, entityId);
70
+ await (0, won_1.refuseWonLead)(this.supabase, organizationId, entityId);
75
71
  }
76
72
  const row = await this.table.insert({
77
- organization_id: this.auth.organization_id,
73
+ organization_id: organizationId,
78
74
  entity_type: entity,
79
75
  entity_id: entityId,
80
76
  key: input.key,
@@ -1,13 +1,8 @@
1
- import { AuditTrailAction, AuditTrailEntityType } from '@managemint-solutions/entities/audit-trail/enum';
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 created_after?: string | null;
8
- readonly created_before?: string | null;
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
- created_after;
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.IsDateString)({}, { message: 'Start date must be a valid ISO 8601 date string' }),
58
- __metadata("design:type", Object)
59
- ], GetAuditTrailDto.prototype, "created_after", void 0);
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, "action", void 0);
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);
@@ -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 (`authenticated` has no grant on
49
- * `notifications.rendered_content`, so the rendered email must not arrive by the back door).
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
- * The `audit_logs` table. Reads are the audit trail endpoint; writes go through `record`, which
71
- * every audited write calls for itself - no feature ever writes an audit line by hand.
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: AuditActor, onError?: AuditErrorHandler | undefined);
90
+ constructor(supabase: SupabaseJsClient, actor: ResourceActor, onError?: AuditErrorHandler | undefined);
78
91
  /**
79
- * Writes the audit row(s) - one insert, however many entries. **Never throws**: a logging
80
- * problem must not fail the business write, so a failure goes to `onAuditError`
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
- list(query: GetAuditTrailDto): Promise<MmsList<AuditTrailEntity>>;
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: AuditActor, audit: AuditResource, options: AuditedTableOptions) => AuditedTable;
186
+ export declare const auditedTable: (supabase: SupabaseJsClient, actor: ResourceActor, audit: AuditResource, options: AuditedTableOptions) => AuditedTable;