@managemint-solutions/sdk 0.8.0 → 0.9.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 CHANGED
@@ -15,7 +15,8 @@ are peer dependencies — the request DTOs (`AddStatusDto`, `UpdateStatusDto`, `
15
15
  `AdditionalDetailsIdDto`, `GetAdditionalDetailsDto`, `AddNoteDto`, `NoteIdDto`,
16
16
  `GetNotesDto`, `SupportingFileIdDto`, `GetSupportingFilesDto`, `GetClientsDto`, `ClientIdDto`,
17
17
  `CreateClientDto`, `UpdateClientDto`, `GetProjectsDto`, `ProjectIdDto`, `CreateProjectDto`,
18
- `UpdateProjectDto`, `GetTasksDto`, `TaskIdDto`, `CreateTaskDto`, `UpdateTaskDto`, `SignInDto`,
18
+ `UpdateProjectDto`, `GetTasksDto`, `TaskIdDto`, `CreateTaskDto`, `UpdateTaskDto`,
19
+ `GetAuditTrailDto`, `SignInDto`,
19
20
  `ExchangeRefreshTokenDto`, `SendPasswordResetEmailDto`, `ExchangeTokenHashDto`,
20
21
  `ResetPasswordDto`) ship with their class-validator
21
22
  decorators so consumers validate against
@@ -198,6 +199,77 @@ The client decodes `{ mms_id, organization_id }` from the JWT itself and scopes
198
199
  by `organization_id`. `supabase.supabase` is the raw `@supabase/supabase-js` client
199
200
  (`TypedSupabaseClient`) — the escape hatch for queries not yet covered by a resource.
200
201
 
202
+ ## Audit
203
+
204
+ Every write a resource makes is logged to `audit_logs` by the SDK itself — there are no database
205
+ triggers and no audit code in any feature. A write records one row carrying the action
206
+ (`INSERT` / `UPDATE` / `ARCHIVE` / `UNARCHIVE` / `DELETE` / `RESTORE`), the entity type and id, who
207
+ did it, and a `changes` diff of the flat table row as real JSON (`{ field: { old, new } }`).
208
+ Inserts and hard deletes record a whole-row snapshot instead, so the trail survives the row.
209
+ Bookkeeping columns are never diffed (`id`, `mms_id`, `organization_id`, `created_at/by`,
210
+ `user_id`, `updated_at`, `last_updated_by`, `archived_*`, `unarchived_*`, `deleted_*`,
211
+ `restored_*`, `entity_type`, `entity_id`); for the tables that hang off another entity (notes,
212
+ additional details, supporting files) the `entity_type`/`entity_id` pair becomes the audit row's
213
+ `related_entity_*` instead.
214
+
215
+ ```ts
216
+ const trail = await supabase.audit.list({
217
+ page: 1,
218
+ page_size: 10,
219
+ entity_type: AuditTrailEntityType.CLIENTS,
220
+ });
221
+ // → MmsList<AuditTrailEntity>, newest first; filter by created_after/before, created_by,
222
+ // action, entity_type and entity_id.
223
+ ```
224
+
225
+ An audit insert **never fails a business write**: if it fails, the error goes to `onAuditError`
226
+ (default `console.error`) and the write still returns normally.
227
+
228
+ ```ts
229
+ const supabase = createSupabaseClient({
230
+ url, key, accessToken,
231
+ onAuditError: (error, entries) => Sentry.captureException(error, { extra: { entries } }),
232
+ });
233
+ ```
234
+
235
+ With the Nest decorator there is nowhere to pass config per request, so the handler is registered
236
+ once at startup — after Sentry is initialised, before the app listens:
237
+
238
+ ```ts
239
+ import { configureSupabaseUserClient } from '@managemint-solutions/sdk/nest';
240
+
241
+ configureSupabaseUserClient({ onAuditError: (error) => Sentry.captureException(error) });
242
+ ```
243
+
244
+ Consumers writing tables the SDK has no resource for (the api's admin-client modules) use the same
245
+ helper over their own client, so they carry no audit code either:
246
+
247
+ ```ts
248
+ import { AuditResource, auditedTable } from '@managemint-solutions/sdk';
249
+ import { AuditTrailAction, AuditTrailEntityType } from '@managemint-solutions/entities/audit-trail/enum';
250
+
251
+ const actor = { mms_id: authUser.mms_id, organization_id: authUser.organization_id }; // mms_id: null for system jobs
252
+ const audit = new AuditResource(adminClient, actor, onAuditError);
253
+ const users = auditedTable(adminClient, actor, audit, {
254
+ table: 'users',
255
+ entityType: AuditTrailEntityType.USER,
256
+ });
257
+
258
+ const row = await users.insert({ ...payload, organization_id: actor.organization_id });
259
+ await users.update(userId, { name: 'Ada' }); // records UPDATE
260
+ await users.update(userId, (before) => ({ active: !before.active })); // payload from the pre-read
261
+ await users.transition(userId, AuditTrailAction.DELETE, { deleted: true }); // records the given action
262
+ const deleted = await users.remove(userId); // hard delete; returns the old row
263
+ ```
264
+
265
+ `update` / `transition` / `remove` pre-read the flat row (by `mms_id`, and by `organization_id`
266
+ unless `scopeByOrganization: false` — the `organizations` table is its own tenant), so a missing
267
+ row surfaces as the usual 406. The write itself still throws `SupabaseClientError` on failure;
268
+ only the audit insert is swallowed. `diffChanges(before, after)` and
269
+ `snapshotChanges(row, 'new' | 'old')` are exported as pure functions if you need them directly.
270
+
271
+ Only what goes through the SDK/API is logged — a raw PostgREST write or a dashboard edit is not.
272
+
201
273
  ## Auth
202
274
 
203
275
  `SupabaseClient` needs the caller's JWT, which the auth flows are there to produce — so sign-in,
@@ -2,13 +2,15 @@ import type { AdditionalDetailSummary, AdditionalDetailsEntity } from '@managemi
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
4
  import type { AuthContext } from '../auth';
5
+ import { type AuditResource } from '../audit';
5
6
  import type { TypedSupabaseClient } from '../client';
6
7
  export * from './dto';
7
8
  export * from './values';
8
9
  export declare class AdditionalDetailsResource {
9
10
  private readonly supabase;
10
11
  private readonly auth;
11
- constructor(supabase: TypedSupabaseClient, auth: AuthContext);
12
+ private readonly table;
13
+ constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
12
14
  getById(additionalDetailsId: string): Promise<AdditionalDetailsEntity>;
13
15
  list(entity: AdditionalDetailsEntityTypes, entityId: string): Promise<AdditionalDetailSummary[]>;
14
16
  create(entity: AdditionalDetailsEntityTypes, entityId: string, input: AddAdditionalDetailsDto): Promise<AdditionalDetailsEntity>;
@@ -15,6 +15,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.AdditionalDetailsResource = void 0;
18
+ const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
19
+ const audit_1 = require("../audit");
18
20
  const errors_1 = require("../errors");
19
21
  const values_1 = require("./values");
20
22
  __exportStar(require("./dto"), exports);
@@ -33,9 +35,14 @@ const withCastValue = (detail) => ({
33
35
  class AdditionalDetailsResource {
34
36
  supabase;
35
37
  auth;
36
- constructor(supabase, auth) {
38
+ table;
39
+ constructor(supabase, auth, audit) {
37
40
  this.supabase = supabase;
38
41
  this.auth = auth;
42
+ this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
43
+ table: 'additional_details',
44
+ entityType: enum_1.AuditTrailEntityType.ADDITIONAL_DETAILS,
45
+ });
39
46
  }
40
47
  async getById(additionalDetailsId) {
41
48
  const { data, error } = await this.supabase
@@ -61,9 +68,7 @@ class AdditionalDetailsResource {
61
68
  return data.map(withCastValue);
62
69
  }
63
70
  async create(entity, entityId, input) {
64
- const { data, error } = await this.supabase
65
- .from('additional_details')
66
- .insert({
71
+ const row = await this.table.insert({
67
72
  organization_id: this.auth.organization_id,
68
73
  entity_type: entity,
69
74
  entity_id: entityId,
@@ -72,40 +77,22 @@ class AdditionalDetailsResource {
72
77
  type: input.type,
73
78
  copyable: input.copyable,
74
79
  created_by: this.auth.mms_id,
75
- })
76
- .select(ADDITIONAL_DETAILS_SELECT)
77
- .single();
78
- if (error)
79
- throw (0, errors_1.mapPostgrestError)(error);
80
- return withCastValue(data);
80
+ });
81
+ return this.getById(row.mms_id);
81
82
  }
82
83
  async update(additionalDetailsId, input) {
83
- const { data, error } = await this.supabase
84
- .from('additional_details')
85
- .update({
84
+ await this.table.update(additionalDetailsId, {
86
85
  key: input.key,
87
86
  value: (0, values_1.serializeValue)(input.value),
88
87
  type: input.type,
89
88
  copyable: input.copyable,
90
89
  updated_at: new Date().toISOString(),
91
90
  last_updated_by: this.auth.mms_id,
92
- })
93
- .eq('mms_id', additionalDetailsId)
94
- .eq('organization_id', this.auth.organization_id)
95
- .select(ADDITIONAL_DETAILS_SELECT)
96
- .single();
97
- if (error)
98
- throw (0, errors_1.mapPostgrestError)(error);
99
- return withCastValue(data);
91
+ });
92
+ return this.getById(additionalDetailsId);
100
93
  }
101
94
  async remove(additionalDetailsId) {
102
- const { error } = await this.supabase
103
- .from('additional_details')
104
- .delete()
105
- .eq('mms_id', additionalDetailsId)
106
- .eq('organization_id', this.auth.organization_id);
107
- if (error)
108
- throw (0, errors_1.mapPostgrestError)(error);
95
+ await this.table.remove(additionalDetailsId);
109
96
  }
110
97
  }
111
98
  exports.AdditionalDetailsResource = AdditionalDetailsResource;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/additional-details/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAcA,sCAA8C;AAC9C,qCAA2D;AAE3D,wCAAsB;AACtB,2CAAyB;AAEzB,2FAA2F;AAC3F,gGAAgG;AAChG,gGAAgG;AAChG,iGAAiG;AACjG,mBAAmB;AACnB,MAAM,yBAAyB,GAC7B,sJAA+J,CAAC;AAElK,MAAM,iCAAiC,GAAG,oCAA6C,CAAC;AAExF,MAAM,aAAa,GAAG,CAA6C,MAAS,EAAE,EAAE,CAAC,CAAC;IAChF,GAAG,MAAM;IACT,KAAK,EAAE,IAAA,wBAAe,EAAC,MAAM,CAAC,IAA8B,EAAE,MAAM,CAAC,KAAK,CAAC;CAC5E,CAAC,CAAC;AAEH,MAAa,yBAAyB;IAEjB;IACA;IAFnB,YACmB,QAA6B,EAC7B,IAAiB;QADjB,aAAQ,GAAR,QAAQ,CAAqB;QAC7B,SAAI,GAAJ,IAAI,CAAa;IACjC,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAC,mBAA2B;QACvC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ;aACxC,IAAI,CAAC,oBAAoB,CAAC;aAC1B,MAAM,CAAC,yBAAyB,CAAC;aACjC,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC;aAChD,EAAE,CAAC,QAAQ,EAAE,mBAAmB,CAAC;aACjC,MAAM,EAAE,CAAC;QACZ,IAAI,KAAK;YAAE,MAAM,IAAA,0BAAiB,EAAC,KAAK,CAAC,CAAC;QAC1C,OAAO,aAAa,CAAC,IAAI,CAAuC,CAAC;IACnE,CAAC;IAED,KAAK,CAAC,IAAI,CACR,MAAoC,EACpC,QAAgB;QAEhB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ;aACxC,IAAI,CAAC,oBAAoB,CAAC;aAC1B,MAAM,CAAC,iCAAiC,CAAC;aACzC,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC;aAChD,EAAE,CAAC,aAAa,EAAE,MAAM,CAAC;aACzB,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC;aACzB,KAAK,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QACtC,IAAI,KAAK;YAAE,MAAM,IAAA,0BAAiB,EAAC,KAAK,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC,GAAG,CAAC,aAAa,CAAyC,CAAC;IACzE,CAAC;IAED,KAAK,CAAC,MAAM,CACV,MAAoC,EACpC,QAAgB,EAChB,KAA8B;QAE9B,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ;aACxC,IAAI,CAAC,oBAAoB,CAAC;aAC1B,MAAM,CAAC;YACN,eAAe,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe;YAC1C,WAAW,EAAE,MAAM;YACnB,SAAS,EAAE,QAAQ;YACnB,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,KAAK,EAAE,IAAA,uBAAc,EAAC,KAAK,CAAC,KAAK,CAAC;YAClC,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM;SAC7B,CAAC;aACD,MAAM,CAAC,yBAAyB,CAAC;aACjC,MAAM,EAAE,CAAC;QACZ,IAAI,KAAK;YAAE,MAAM,IAAA,0BAAiB,EAAC,KAAK,CAAC,CAAC;QAC1C,OAAO,aAAa,CAAC,IAAI,CAAuC,CAAC;IACnE,CAAC;IAED,KAAK,CAAC,MAAM,CACV,mBAA2B,EAC3B,KAAiC;QAEjC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ;aACxC,IAAI,CAAC,oBAAoB,CAAC;aAC1B,MAAM,CAAC;YACN,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,KAAK,EAAE,IAAA,uBAAc,EAAC,KAAK,CAAC,KAAK,CAAC;YAClC,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACpC,eAAe,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM;SAClC,CAAC;aACD,EAAE,CAAC,QAAQ,EAAE,mBAAmB,CAAC;aACjC,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC;aAChD,MAAM,CAAC,yBAAyB,CAAC;aACjC,MAAM,EAAE,CAAC;QACZ,IAAI,KAAK;YAAE,MAAM,IAAA,0BAAiB,EAAC,KAAK,CAAC,CAAC;QAC1C,OAAO,aAAa,CAAC,IAAI,CAAuC,CAAC;IACnE,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,mBAA2B;QACtC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ;aAClC,IAAI,CAAC,oBAAoB,CAAC;aAC1B,MAAM,EAAE;aACR,EAAE,CAAC,QAAQ,EAAE,mBAAmB,CAAC;aACjC,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QACpD,IAAI,KAAK;YAAE,MAAM,IAAA,0BAAiB,EAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;CACF;AArFD,8DAqFC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/additional-details/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { AuditTrailAction, AuditTrailEntityType } from '@managemint-solutions/entities/audit-trail/enum';
2
+ import type { GetAuditTrailDto as GetAuditTrailDtoType } from '@managemint-solutions/entities/audit-trail/dto';
3
+ export declare class GetAuditTrailDto implements GetAuditTrailDtoType {
4
+ readonly search?: string | null;
5
+ readonly page: number;
6
+ 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;
13
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.GetAuditTrailDto = void 0;
13
+ const class_validator_1 = require("class-validator");
14
+ const class_transformer_1 = require("class-transformer");
15
+ const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
16
+ const validators_1 = require("../validators");
17
+ // class-validator version of the shared GetAuditTrailDto type from
18
+ // @managemint-solutions/entities, so the wire contract cannot drift.
19
+ class GetAuditTrailDto {
20
+ search;
21
+ page = 1;
22
+ page_size = 10;
23
+ created_after;
24
+ created_before;
25
+ created_by;
26
+ action;
27
+ entity_type;
28
+ entity_id;
29
+ }
30
+ 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
+ __decorate([
40
+ (0, class_validator_1.IsOptional)(),
41
+ (0, class_transformer_1.Transform)(({ value }) => value ? Number(value) : 1, { toClassOnly: true }),
42
+ (0, class_validator_1.IsNumber)({}, { message: 'Page must be a number' }),
43
+ (0, class_validator_1.Min)(1, { message: 'Page must be at least 1' }),
44
+ __metadata("design:type", Number)
45
+ ], GetAuditTrailDto.prototype, "page", void 0);
46
+ __decorate([
47
+ (0, class_validator_1.IsOptional)(),
48
+ (0, class_transformer_1.Transform)(({ value }) => value ? Number(value) : 10, { toClassOnly: true }),
49
+ (0, class_validator_1.IsNumber)({}, { message: 'Page size must be a number' }),
50
+ (0, class_validator_1.Min)(1, { message: 'Page size must be at least 1' }),
51
+ (0, class_validator_1.Max)(100, { message: 'Page size cannot exceed 100' }),
52
+ __metadata("design:type", Number)
53
+ ], GetAuditTrailDto.prototype, "page_size", void 0);
54
+ __decorate([
55
+ (0, class_validator_1.IsOptional)(),
56
+ (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
+ (0, class_validator_1.IsUUID)('4', { message: 'Created by must be a valid UUID v4' }),
70
+ __metadata("design:type", Object)
71
+ ], GetAuditTrailDto.prototype, "created_by", void 0);
72
+ __decorate([
73
+ (0, class_validator_1.IsOptional)(),
74
+ (0, validators_1.IsNullable)(),
75
+ (0, class_validator_1.IsEnum)(enum_1.AuditTrailAction, { message: `Action must be one of: ${Object.values(enum_1.AuditTrailAction).join(', ')}` }),
76
+ __metadata("design:type", Object)
77
+ ], GetAuditTrailDto.prototype, "action", void 0);
78
+ __decorate([
79
+ (0, class_validator_1.IsOptional)(),
80
+ (0, validators_1.IsNullable)(),
81
+ (0, class_validator_1.IsEnum)(enum_1.AuditTrailEntityType, { message: `Entity type must be one of: ${Object.values(enum_1.AuditTrailEntityType).join(', ')}` }),
82
+ __metadata("design:type", Object)
83
+ ], GetAuditTrailDto.prototype, "entity_type", void 0);
84
+ __decorate([
85
+ (0, class_validator_1.IsOptional)(),
86
+ (0, validators_1.IsNullable)(),
87
+ (0, class_validator_1.IsUUID)('4', { message: 'Entity ID must be a valid UUID v4' }),
88
+ __metadata("design:type", Object)
89
+ ], GetAuditTrailDto.prototype, "entity_id", void 0);
90
+ //# sourceMappingURL=dto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dto.js","sourceRoot":"","sources":["../../src/audit/dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAA+H;AAC/H,yDAA8C;AAC9C,0EAAyG;AAEzG,8CAA2C;AAE3C,mEAAmE;AACnE,qEAAqE;AACrE,MAAa,gBAAgB;IAMlB,MAAM,CAAiB;IAMvB,IAAI,GAAW,CAAC,CAAC;IAOjB,SAAS,GAAW,EAAE,CAAC;IAKvB,aAAa,CAAiB;IAK9B,cAAc,CAAiB;IAK/B,UAAU,CAAiB;IAK3B,MAAM,CAA2B;IAKjC,WAAW,CAA+B;IAK1C,SAAS,CAAiB;CACpC;AAlDD,4CAkDC;AA5CU;IALR,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAU,GAAE;IACZ,IAAA,0BAAQ,EAAC,EAAE,OAAO,EAAE,+BAA+B,EAAE,CAAC;IACtD,IAAA,2BAAS,EAAC,CAAC,EAAE,EAAE,OAAO,EAAE,8BAA8B,EAAE,CAAC;IACzD,IAAA,2BAAS,EAAC,GAAG,EAAE,EAAE,OAAO,EAAE,2CAA2C,EAAE,CAAC;;gDACzC;AAMvB;IAJR,IAAA,4BAAU,GAAE;IACZ,IAAA,6BAAS,EAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IAC1E,IAAA,0BAAQ,EAAC,EAAE,EAAE,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC;IAClD,IAAA,qBAAG,EAAC,CAAC,EAAE,EAAE,OAAO,EAAE,yBAAyB,EAAE,CAAC;;8CACrB;AAOjB;IALR,IAAA,4BAAU,GAAE;IACZ,IAAA,6BAAS,EAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IAC3E,IAAA,0BAAQ,EAAC,EAAE,EAAE,EAAE,OAAO,EAAE,4BAA4B,EAAE,CAAC;IACvD,IAAA,qBAAG,EAAC,CAAC,EAAE,EAAE,OAAO,EAAE,8BAA8B,EAAE,CAAC;IACnD,IAAA,qBAAG,EAAC,GAAG,EAAE,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAC;;mDACrB;AAKvB;IAHR,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAU,GAAE;IACZ,IAAA,8BAAY,EAAC,EAAE,EAAE,EAAE,OAAO,EAAE,iDAAiD,EAAE,CAAC;;uDAC1C;AAK9B;IAHR,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAU,GAAE;IACZ,IAAA,8BAAY,EAAC,EAAE,EAAE,EAAE,OAAO,EAAE,+CAA+C,EAAE,CAAC;;wDACvC;AAK/B;IAHR,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAU,GAAE;IACZ,IAAA,wBAAM,EAAC,GAAG,EAAE,EAAE,OAAO,EAAE,oCAAoC,EAAE,CAAC;;oDAC3B;AAK3B;IAHR,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAU,GAAE;IACZ,IAAA,wBAAM,EAAC,uBAAgB,EAAE,EAAE,OAAO,EAAE,0BAA0B,MAAM,CAAC,MAAM,CAAC,uBAAgB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;;gDACpE;AAKjC;IAHR,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAU,GAAE;IACZ,IAAA,wBAAM,EAAC,2BAAoB,EAAE,EAAE,OAAO,EAAE,+BAA+B,MAAM,CAAC,MAAM,CAAC,2BAAoB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;;qDACxE;AAK1C;IAHR,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAU,GAAE;IACZ,IAAA,wBAAM,EAAC,GAAG,EAAE,EAAE,OAAO,EAAE,mCAAmC,EAAE,CAAC;;mDAC3B"}
@@ -0,0 +1,89 @@
1
+ import type { SupabaseClient as SupabaseJsClient } from '@supabase/supabase-js';
2
+ import type { AuditTrailEntity } from '@managemint-solutions/entities/audit-trail';
3
+ import { AuditTrailAction, type AuditTrailEntityType } from '@managemint-solutions/entities/audit-trail/enum';
4
+ import type { MmsList } from '@managemint-solutions/entities/common';
5
+ import type { GetAuditTrailDto } from './dto';
6
+ export * from './dto';
7
+ /**
8
+ * Who a write is attributed to. `AuthContext` is assignable to it; the api's admin-client
9
+ * modules build one by hand (system jobs pass `mms_id: null` — `created_by` is nullable).
10
+ */
11
+ export type AuditActor = {
12
+ mms_id: string | null;
13
+ organization_id: string;
14
+ };
15
+ /** A flat database row — always `select('*')`, never an entity shape with embedded joins. */
16
+ export type AuditRow = Record<string, unknown>;
17
+ export type AuditChanges = Record<string, {
18
+ old: unknown;
19
+ new: unknown;
20
+ }>;
21
+ export type AuditEntry = {
22
+ action: AuditTrailAction;
23
+ entityType: AuditTrailEntityType;
24
+ entityId: string;
25
+ changes: AuditChanges;
26
+ relatedEntityType?: string | null;
27
+ relatedEntityId?: string | null;
28
+ };
29
+ /** Called when the audit insert itself fails. The business write has already succeeded. */
30
+ export type AuditErrorHandler = (error: unknown, entries: AuditEntry[]) => void;
31
+ /**
32
+ * Bookkeeping columns a diff never reports: identity, tenancy and the who/when stamps of the
33
+ * transition itself (the action already says what happened), plus the `entity_type`/`entity_id`
34
+ * pair, which becomes `related_entity_*` on the audit row instead.
35
+ */
36
+ export declare const AUDIT_EXCLUDED_COLUMNS: ReadonlySet<string>;
37
+ /**
38
+ * The fields that differ between two flat rows, as `{ field: { old, new } }`. Values stay real
39
+ * JSON (booleans stay booleans, jsonb stays an object) and are compared by serialized value, so
40
+ * arrays and objects are equal when their contents are.
41
+ */
42
+ export declare const diffChanges: (before: AuditRow, after: AuditRow) => AuditChanges;
43
+ /**
44
+ * A whole row as a change set — `'new'` for an insert, `'old'` for a hard delete — so the trail
45
+ * stays self-contained once the row is gone. Columns that hold nothing are left out.
46
+ */
47
+ export declare const snapshotChanges: (row: AuditRow, side: "new" | "old") => AuditChanges;
48
+ /**
49
+ * The `audit_logs` table. Reads are the audit trail endpoint; writes go through `record`, which
50
+ * every audited write calls for itself — no feature ever writes an audit line by hand.
51
+ */
52
+ export declare class AuditResource {
53
+ private readonly supabase;
54
+ private readonly actor;
55
+ private readonly onError?;
56
+ constructor(supabase: SupabaseJsClient, actor: AuditActor, onError?: AuditErrorHandler | undefined);
57
+ /**
58
+ * Writes the audit row(s) — one insert, however many entries. **Never throws**: a logging
59
+ * problem must not fail the business write, so a failure goes to `onAuditError`
60
+ * (default `console.error`) instead.
61
+ */
62
+ record(entry: AuditEntry | AuditEntry[]): Promise<void>;
63
+ list(query: GetAuditTrailDto): Promise<MmsList<AuditTrailEntity>>;
64
+ }
65
+ export type AuditedTableOptions = {
66
+ table: string;
67
+ entityType: AuditTrailEntityType;
68
+ /**
69
+ * Whether rows are scoped by `organization_id` (the default). `false` for the `organizations`
70
+ * table itself, whose own `mms_id` is the organization.
71
+ */
72
+ scopeByOrganization?: boolean;
73
+ };
74
+ /** The payload to write, or a function of the pre-read row (`togglePin`'s `!before.pinned`). */
75
+ type AuditedPayload = AuditRow | ((before: AuditRow) => AuditRow);
76
+ export type AuditedTable = {
77
+ insert(payload: AuditRow): Promise<AuditRow>;
78
+ insertMany(payloads: AuditRow[]): Promise<AuditRow[]>;
79
+ update(id: string, payload: AuditedPayload): Promise<AuditRow>;
80
+ transition(id: string, action: AuditTrailAction, payload: AuditedPayload): Promise<AuditRow>;
81
+ /** Hard delete. Returns the row as it was, so the caller can still act on it. */
82
+ remove(id: string): Promise<AuditRow>;
83
+ };
84
+ /**
85
+ * The audited write helper every resource writes through: it does the pre-read, the write, the
86
+ * diff and the `audit_logs` insert itself, so no feature carries audit code. The write fails
87
+ * loudly (`SupabaseClientError`); only the audit insert is swallowed.
88
+ */
89
+ export declare const auditedTable: (supabase: SupabaseJsClient, actor: AuditActor, audit: AuditResource, options: AuditedTableOptions) => AuditedTable;