@managemint-solutions/sdk 0.51.0 → 0.53.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/dist/index.js CHANGED
@@ -7,9 +7,9 @@
7
7
  // their classes are not exported: a consumer never constructs one, and every export is a contract
8
8
  // to keep. The row and input types they produce and accept are exported for annotations.
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.UpdatePermissionsDto = exports.PermissionsResource = exports.UpdateOrganizationDto = exports.CreateOrganizationDto = exports.NotificationIdDto = exports.GetNotificationsDto = exports.NoteIdDto = exports.GetNotesDto = exports.AddNoteDto = exports.UpdateAdditionalDetailsDto = exports.IsAdditionalDetailsValueByType = exports.GetAdditionalDetailsDto = exports.AdditionalDetailsIdDto = exports.AddAdditionalDetailsDto = exports.UpdateLabelDto = exports.LabelIdDto = exports.GetLabelsDto = exports.AddLabelDto = exports.UpdateStatusDto = exports.StatusIdDto = exports.ReorderStatusesDto = exports.GetStatusesDto = exports.AddStatusDto = exports.CreateSupportRequestDto = exports.UpdateConfigDto = exports.CreateConfigDto = exports.CreateFeatureRequestDto = exports.InvoiceIdDto = exports.GetInvoicesQueryDto = exports.ReinstateSubscriptionDto = exports.PurchaseModuleDto = exports.PaymentIdDto = exports.InitialPaymentDto = exports.GetPaymentsQueryDto = exports.CancelSubscriptionDto = exports.GetAuditTrailDto = exports.IsNullable = exports.EmptyToNull = exports.SupabaseClientError = exports.createSupabaseServiceClient = exports.SupabaseServiceClient = exports.SignInDto = exports.SendPasswordResetEmailDto = exports.ResetPasswordDto = exports.ExchangeTokenHashDto = exports.ExchangeRefreshTokenDto = exports.createSupabaseAuthClient = exports.SupabaseAuthClient = exports.createSupabaseClient = exports.SupabaseClient = void 0;
11
- exports.isWithinPeriod = exports.initialUntil = exports.cyclePeriodFor = exports.countWorkingDays = exports.completedMonths = exports.addMonths = exports.GetPublicHolidaysDto = exports.UpdateLeaveTypeDto = exports.UpdateLeaveRequestDto = exports.UpdateLeaveEntitlementDto = exports.ReviewLeaveRequestDto = exports.LeaveTypeIdDto = exports.LeaveRequestIdDto = exports.LeaveEntitlementIdDto = exports.GetLeaveTypesDto = exports.GetLeaveRequestsDto = exports.GetLeaveEntitlementsDto = exports.GetLeaveCalendarDto = exports.CreateLeaveTypeDto = exports.CreateLeaveRequestDto = exports.CreateLeaveEntitlementDto = exports.CreateLeaveAdjustmentDto = exports.BulkCreateLeaveEntitlementsDto = exports.UpdateTaskDto = exports.TaskLinkIdDto = exports.TaskIdDto = exports.GetTasksDto = exports.CreateTaskDto = exports.AddTaskLinkDto = exports.UpdateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = exports.CreateProjectDto = exports.UpdateLeadDto = exports.LeadIdDto = exports.GetLeadsDto = exports.CreateLeadDto = exports.ConvertLeadDto = exports.UpdateClientDto = exports.GetClientsDto = exports.CreateClientDto = exports.ClientIdDto = exports.SupportingFileNamesDto = exports.SupportingFileIdDto = exports.GetSupportingFilesDto = exports.UserIdDto = exports.UpdateUserDto = exports.UpdateMyProfileDto = exports.GetUsersQueryDto = exports.AddUserDto = void 0;
12
- exports.monthlyTotalMinor = exports.weekStartOf = exports.minutesBetween = exports.UpdateTimesheetEntryDto = exports.TimesheetIdDto = exports.TimesheetEntryIdDto = exports.ReviewTimesheetDto = exports.GetTimesheetsDto = exports.GetTimesheetReportDto = exports.CreateTimesheetEntryDto = exports.todayInJohannesburg = exports.shiftDays = exports.proRataDays = void 0;
10
+ exports.PermissionsResource = exports.UpdateOrganizationDto = exports.CreateOrganizationDto = exports.NotificationIdDto = exports.GetNotificationsDto = exports.NoteIdDto = exports.GetNotesDto = exports.AddNoteDto = exports.UpdateAdditionalDetailsDto = exports.IsAdditionalDetailsValueByType = exports.GetAdditionalDetailsDto = exports.AdditionalDetailsIdDto = exports.AddAdditionalDetailsDto = exports.UpdateLabelDto = exports.LabelIdDto = exports.GetLabelsDto = exports.AddLabelDto = exports.UpdateStatusDto = exports.StatusIdDto = exports.ReorderStatusesDto = exports.GetStatusesDto = exports.AddStatusDto = exports.CreateSupportRequestDto = exports.UpdateConfigDto = exports.CreateConfigDto = exports.CreateFeatureRequestDto = exports.InvoiceIdDto = exports.GetInvoicesQueryDto = exports.ReinstateSubscriptionDto = exports.PurchaseModuleDto = exports.PaymentIdDto = exports.InitialPaymentDto = exports.GetPaymentsQueryDto = exports.CancelSubscriptionDto = exports.withAuditBatch = exports.GetAuditTrailDto = exports.IsNullable = exports.EmptyToNull = exports.SupabaseClientError = exports.createSupabaseServiceClient = exports.SupabaseServiceClient = exports.SignInDto = exports.SendPasswordResetEmailDto = exports.ResetPasswordDto = exports.ExchangeTokenHashDto = exports.ExchangeRefreshTokenDto = exports.createSupabaseAuthClient = exports.SupabaseAuthClient = exports.createSupabaseClient = exports.SupabaseClient = void 0;
11
+ exports.initialUntil = exports.cyclePeriodFor = exports.countWorkingDays = exports.completedMonths = exports.addMonths = exports.GetPublicHolidaysDto = exports.UpdateLeaveTypeDto = exports.UpdateLeaveRequestDto = exports.UpdateLeaveEntitlementDto = exports.ReviewLeaveRequestDto = exports.LeaveTypeIdDto = exports.LeaveRequestIdDto = exports.LeaveEntitlementIdDto = exports.GetLeaveTypesDto = exports.GetLeaveRequestsDto = exports.GetLeaveEntitlementsDto = exports.GetLeaveCalendarDto = exports.CreateLeaveTypeDto = exports.CreateLeaveRequestDto = exports.CreateLeaveEntitlementDto = exports.CreateLeaveAdjustmentDto = exports.BulkCreateLeaveEntitlementsDto = exports.UpdateTaskDto = exports.TaskLinkIdDto = exports.TaskIdDto = exports.GetTasksDto = exports.CreateTaskDto = exports.AddTaskLinkDto = exports.UpdateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = exports.CreateProjectDto = exports.UpdateLeadDto = exports.LeadIdDto = exports.GetLeadsDto = exports.CreateLeadDto = exports.ConvertLeadDto = exports.UpdateClientDto = exports.GetClientsDto = exports.CreateClientDto = exports.ClientIdDto = exports.SupportingFileNamesDto = exports.SupportingFileIdDto = exports.GetSupportingFilesDto = exports.UserIdDto = exports.UpdateUserDto = exports.UpdateMyProfileDto = exports.GetUsersQueryDto = exports.AddUserDto = exports.UpdatePermissionsDto = void 0;
12
+ exports.monthlyTotalMinor = exports.weekStartOf = exports.minutesBetween = exports.UpdateTimesheetEntryDto = exports.TimesheetIdDto = exports.TimesheetEntryIdDto = exports.ReviewTimesheetDto = exports.GetTimesheetsDto = exports.GetTimesheetReportDto = exports.CreateTimesheetEntryDto = exports.todayInJohannesburg = exports.shiftDays = exports.proRataDays = exports.isWithinPeriod = void 0;
13
13
  var client_1 = require("./client");
14
14
  Object.defineProperty(exports, "SupabaseClient", { enumerable: true, get: function () { return client_1.SupabaseClient; } });
15
15
  Object.defineProperty(exports, "createSupabaseClient", { enumerable: true, get: function () { return client_1.createSupabaseClient; } });
@@ -32,6 +32,7 @@ Object.defineProperty(exports, "EmptyToNull", { enumerable: true, get: function
32
32
  Object.defineProperty(exports, "IsNullable", { enumerable: true, get: function () { return validators_1.IsNullable; } });
33
33
  var audit_1 = require("./audit");
34
34
  Object.defineProperty(exports, "GetAuditTrailDto", { enumerable: true, get: function () { return audit_1.GetAuditTrailDto; } });
35
+ Object.defineProperty(exports, "withAuditBatch", { enumerable: true, get: function () { return audit_1.withAuditBatch; } });
35
36
  var billing_1 = require("./billing");
36
37
  Object.defineProperty(exports, "CancelSubscriptionDto", { enumerable: true, get: function () { return billing_1.CancelSubscriptionDto; } });
37
38
  Object.defineProperty(exports, "GetPaymentsQueryDto", { enumerable: true, get: function () { return billing_1.GetPaymentsQueryDto; } });
@@ -3,7 +3,7 @@ import type { MmsList } from '@managemint-solutions/entities/common';
3
3
  import type { LeadEntity, LeadSummary } from '@managemint-solutions/entities/leads';
4
4
  import type { AuthContext } from '../auth';
5
5
  import type { AdditionalDetailsResource } from '../additional-details';
6
- import { type AuditResource } from '../audit';
6
+ import { EntityAuditTrail, type AuditResource } from '../audit';
7
7
  import type { ClientsResource } from '../clients';
8
8
  import type { TypedSupabaseClient } from '../client';
9
9
  import type { ConvertLeadDto, CreateLeadDto, GetLeadsDto, UpdateLeadDto } from './dto';
@@ -11,9 +11,10 @@ export * from './dto';
11
11
  export declare class LeadsResource {
12
12
  private readonly supabase;
13
13
  private readonly auth;
14
- private readonly audit;
15
14
  private readonly additionalDetails;
16
15
  private readonly table;
16
+ /** This record's history - its own rows and the rows that hang off it. */
17
+ readonly audit: EntityAuditTrail;
17
18
  constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource, additionalDetails: AdditionalDetailsResource);
18
19
  list(query: GetLeadsDto): Promise<MmsList<LeadSummary>>;
19
20
  getById(leadId: string): Promise<LeadEntity>;
@@ -106,18 +106,19 @@ const refuse = (status, message) => new errors_1.SupabaseClientError({ status, e
106
106
  class LeadsResource {
107
107
  supabase;
108
108
  auth;
109
- audit;
110
109
  additionalDetails;
111
110
  table;
111
+ /** This record's history - its own rows and the rows that hang off it. */
112
+ audit;
112
113
  constructor(supabase, auth, audit, additionalDetails) {
113
114
  this.supabase = supabase;
114
115
  this.auth = auth;
115
- this.audit = audit;
116
116
  this.additionalDetails = additionalDetails;
117
117
  this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
118
118
  table: 'leads',
119
119
  entityType: enum_6.AuditTrailEntityType.LEADS,
120
120
  });
121
+ this.audit = new audit_1.EntityAuditTrail(supabase, auth, enum_6.AuditTrailEntityType.LEADS);
121
122
  }
122
123
  async list(query) {
123
124
  const page = query.page;
@@ -27,6 +27,7 @@ class LeaveAdjustmentsResource {
27
27
  this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
28
28
  table: 'leave_adjustments',
29
29
  entityType: enum_1.AuditTrailEntityType.LEAVE_ADJUSTMENTS,
30
+ related: { entityType: enum_1.AuditTrailEntityType.LEAVE_ENTITLEMENTS, column: 'leave_entitlement_id' },
30
31
  });
31
32
  }
32
33
  /** The adjustments of the given entitlements, oldest first. */
@@ -1,7 +1,7 @@
1
1
  import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { LeaveEntitlementEntity, LeavePeriod } from '@managemint-solutions/entities/leave';
3
3
  import { LeaveGrantMethod } from '@managemint-solutions/entities/leave/enum';
4
- import { type AuditActor, type AuditResource } from '../audit';
4
+ import { EntityAuditTrail, type AuditActor, type AuditResource } from '../audit';
5
5
  import type { TypedSupabaseClient } from '../client';
6
6
  import type { GetLeaveEntitlementsDto, UpdateLeaveEntitlementDto } from './dto';
7
7
  /** The create input after the api has resolved the defaults and copied the type's policy on. */
@@ -21,6 +21,8 @@ export declare class LeaveEntitlementsResource {
21
21
  private readonly supabase;
22
22
  private readonly auth;
23
23
  private readonly table;
24
+ /** This record's history - its own rows and the rows that hang off it. */
25
+ readonly audit: EntityAuditTrail;
24
26
  constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
25
27
  list(query: GetLeaveEntitlementsDto): Promise<MmsList<LeaveEntitlementEntity>>;
26
28
  getById(leaveEntitlementId: string): Promise<LeaveEntitlementEntity>;
@@ -38,6 +38,8 @@ class LeaveEntitlementsResource {
38
38
  supabase;
39
39
  auth;
40
40
  table;
41
+ /** This record's history - its own rows and the rows that hang off it. */
42
+ audit;
41
43
  constructor(supabase, auth, audit) {
42
44
  this.supabase = supabase;
43
45
  this.auth = auth;
@@ -45,6 +47,7 @@ class LeaveEntitlementsResource {
45
47
  table: 'leave_entitlements',
46
48
  entityType: enum_1.AuditTrailEntityType.LEAVE_ENTITLEMENTS,
47
49
  });
50
+ this.audit = new audit_1.EntityAuditTrail(supabase, auth, enum_1.AuditTrailEntityType.LEAVE_ENTITLEMENTS);
48
51
  }
49
52
  async list(query) {
50
53
  const page = query.page;
@@ -1,7 +1,7 @@
1
1
  import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { LeaveCalendarEntry, LeaveRequestEntity } from '@managemint-solutions/entities/leave';
3
3
  import { LeaveRequestStatus, LeaveReviewDecision } from '@managemint-solutions/entities/leave/enum';
4
- import { type AuditActor, type AuditResource } from '../audit';
4
+ import { EntityAuditTrail, type AuditActor, type AuditResource } from '../audit';
5
5
  import type { TypedSupabaseClient } from '../client';
6
6
  import type { GetLeaveCalendarDto, GetLeaveRequestsDto } from './dto';
7
7
  /** A request as the api inserts it: the caller's own, with the days already counted. */
@@ -39,6 +39,8 @@ export declare class LeaveRequestsResource {
39
39
  private readonly supabase;
40
40
  private readonly auth;
41
41
  private readonly table;
42
+ /** This record's history - its own rows and the rows that hang off it. */
43
+ readonly audit: EntityAuditTrail;
42
44
  constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
43
45
  list(query: GetLeaveRequestsDto, options?: ListOptions): Promise<MmsList<LeaveRequestEntity>>;
44
46
  getById(leaveRequestId: string): Promise<LeaveRequestEntity>;
@@ -49,6 +49,8 @@ class LeaveRequestsResource {
49
49
  supabase;
50
50
  auth;
51
51
  table;
52
+ /** This record's history - its own rows and the rows that hang off it. */
53
+ audit;
52
54
  constructor(supabase, auth, audit) {
53
55
  this.supabase = supabase;
54
56
  this.auth = auth;
@@ -56,6 +58,7 @@ class LeaveRequestsResource {
56
58
  table: 'leave_requests',
57
59
  entityType: enum_1.AuditTrailEntityType.LEAVE_REQUESTS,
58
60
  });
61
+ this.audit = new audit_1.EntityAuditTrail(supabase, auth, enum_1.AuditTrailEntityType.LEAVE_REQUESTS);
59
62
  }
60
63
  async list(query, options = {}) {
61
64
  const page = query.page;
@@ -1,4 +1,4 @@
1
- import { type AuditActor, type AuditResource } from '../audit';
1
+ import type { AuditActor } from '../audit';
2
2
  import type { TypedSupabaseClient } from '../client';
3
3
  /**
4
4
  * Everything Paystack knows an organization by, plus the two secrets the billing service acts
@@ -31,15 +31,14 @@ export type PaymentMethodLookupColumn = 'paystack_customer_code' | 'paystack_sub
31
31
  * policy. Only the service role can reach it.
32
32
  *
33
33
  * Service-role only, like the invites - this lives on the organization scope of the service
34
- * client and never on a caller's client. Writes are audited against the organization (the row
35
- * has no identity of its own), and the audit diff redacts every `paystack_*` column, so the trail
36
- * records that a value changed and never what it changed to.
34
+ * client and never on a caller's client. Writes are not audited: every column is provider
35
+ * machinery, and an organization's subscription history is the billing ledger, not its audit
36
+ * trail (`untracked` on the organizations table, docs/11-audit-trail.md).
37
37
  */
38
38
  export declare class OrganizationPaymentMethodsResource {
39
39
  private readonly supabase;
40
40
  private readonly actor;
41
- private readonly audit;
42
- constructor(supabase: TypedSupabaseClient, actor: AuditActor, audit: AuditResource);
41
+ constructor(supabase: TypedSupabaseClient, actor: AuditActor);
43
42
  /** The actor organization's payment method; every column null when nothing has been stored yet. */
44
43
  get(): Promise<OrganizationPaymentMethod>;
45
44
  /**
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OrganizationPaymentMethodsResource = exports.PAYMENT_METHOD_COLUMNS = void 0;
4
- const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
5
- const audit_1 = require("../audit");
6
4
  const errors_1 = require("../errors");
7
5
  exports.PAYMENT_METHOD_COLUMNS = [
8
6
  'paystack_customer_code',
@@ -33,18 +31,16 @@ const EMPTY = {
33
31
  * policy. Only the service role can reach it.
34
32
  *
35
33
  * Service-role only, like the invites - this lives on the organization scope of the service
36
- * client and never on a caller's client. Writes are audited against the organization (the row
37
- * has no identity of its own), and the audit diff redacts every `paystack_*` column, so the trail
38
- * records that a value changed and never what it changed to.
34
+ * client and never on a caller's client. Writes are not audited: every column is provider
35
+ * machinery, and an organization's subscription history is the billing ledger, not its audit
36
+ * trail (`untracked` on the organizations table, docs/11-audit-trail.md).
39
37
  */
40
38
  class OrganizationPaymentMethodsResource {
41
39
  supabase;
42
40
  actor;
43
- audit;
44
- constructor(supabase, actor, audit) {
41
+ constructor(supabase, actor) {
45
42
  this.supabase = supabase;
46
43
  this.actor = actor;
47
- this.audit = audit;
48
44
  }
49
45
  /** The actor organization's payment method; every column null when nothing has been stored yet. */
50
46
  async get() {
@@ -73,15 +69,6 @@ class OrganizationPaymentMethodsResource {
73
69
  }, { onConflict: 'organization_id' });
74
70
  if (error)
75
71
  throw (0, errors_1.mapPostgrestError)(error);
76
- const changes = (0, audit_1.diffChanges)(before, after);
77
- if (Object.keys(changes).length === 0)
78
- return;
79
- await this.audit.record({
80
- action: enum_1.AuditTrailAction.UPDATE,
81
- entityType: enum_1.AuditTrailEntityType.ORGANIZATION,
82
- entityId: this.actor.organization_id,
83
- changes,
84
- });
85
72
  }
86
73
  }
87
74
  exports.OrganizationPaymentMethodsResource = OrganizationPaymentMethodsResource;
@@ -1,5 +1,5 @@
1
1
  import type { SeatReservation } from '@managemint-solutions/entities/billing';
2
- import { type AuditActor, type AuditResource, type AuditRow } from '../audit';
2
+ import { type AuditActor, type AuditResource, type AuditRow, EntityAuditTrail } from '../audit';
3
3
  import type { TypedSupabaseClient } from '../client';
4
4
  export * from './dto';
5
5
  /** The organizations row as `ORGANIZATION_SELECT` produces it, before the api shapes it. */
@@ -18,6 +18,8 @@ export declare class OrganizationsResource {
18
18
  private readonly supabase;
19
19
  private readonly auth;
20
20
  private readonly table;
21
+ /** This record's history - its own rows and the rows that hang off it. */
22
+ readonly audit: EntityAuditTrail;
21
23
  constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
22
24
  getById(organizationId: string): Promise<OrganizationRow>;
23
25
  /** The caller's own organization, from the auth context. */
@@ -20,12 +20,34 @@ const audit_1 = require("../audit");
20
20
  const errors_1 = require("../errors");
21
21
  const transforms_1 = require("../transforms");
22
22
  __exportStar(require("./dto"), exports);
23
+ // The subscription machinery on the row - what the billing flows and the Paystack webhooks
24
+ // move. Its history is the billing ledger (billing_transactions, invoices, billing_events);
25
+ // the organization's audit trail is its profile, so these are never recorded there.
26
+ const SUBSCRIPTION_COLUMNS = [
27
+ 'billing_status',
28
+ 'billing_status_updated_at',
29
+ 'billing_cycle_locked',
30
+ 'payment_frequency',
31
+ 'payment_override',
32
+ 'last_payment_date',
33
+ 'next_payment_date',
34
+ 'access_expires_at',
35
+ 'active_seat_count',
36
+ 'paid_seat_count',
37
+ 'modules',
38
+ 'pricebook_id',
39
+ 'card_brand',
40
+ 'card_description',
41
+ 'card_expiry_date',
42
+ 'card_expiring_soon',
43
+ ];
23
44
  // The organizations row *is* the organization: it has no organization_id column, hence
24
45
  // `scopeByOrganization: false` wherever it is audited.
25
46
  const ORGANIZATIONS_TABLE = {
26
47
  table: 'organizations',
27
48
  entityType: enum_1.AuditTrailEntityType.ORGANIZATION,
28
49
  scopeByOrganization: false,
50
+ untracked: SUBSCRIPTION_COLUMNS,
29
51
  };
30
52
  // The api maps this row into OrganizationEntity: the module keys stored here are priced
31
53
  // against the organization's pricebook, and the flat address columns are nested. That is
@@ -81,10 +103,13 @@ class OrganizationsResource {
81
103
  supabase;
82
104
  auth;
83
105
  table;
106
+ /** This record's history - its own rows and the rows that hang off it. */
107
+ audit;
84
108
  constructor(supabase, auth, audit) {
85
109
  this.supabase = supabase;
86
110
  this.auth = auth;
87
111
  this.table = (0, audit_1.auditedTable)(supabase, auth, audit, ORGANIZATIONS_TABLE);
112
+ this.audit = new audit_1.EntityAuditTrail(supabase, auth, enum_1.AuditTrailEntityType.ORGANIZATION);
88
113
  }
89
114
  async getById(organizationId) {
90
115
  const { data, error } = await this.supabase
@@ -203,11 +228,14 @@ class OrganizationSignupResource {
203
228
  if (error)
204
229
  throw (0, errors_1.mapPostgrestError)(error);
205
230
  const row = data;
231
+ const changes = (0, audit_1.snapshotChanges)(row, 'new');
232
+ for (const column of SUBSCRIPTION_COLUMNS)
233
+ delete changes[column];
206
234
  await this.auditFor(row.mms_id).record({
207
235
  action: enum_1.AuditTrailAction.INSERT,
208
236
  entityType: enum_1.AuditTrailEntityType.ORGANIZATION,
209
237
  entityId: row.mms_id,
210
- changes: (0, audit_1.snapshotChanges)(row, 'new'),
238
+ changes,
211
239
  });
212
240
  return (0, audit_1.withoutInternalId)(row);
213
241
  }
@@ -22,6 +22,8 @@ __exportStar(require("./dto"), exports);
22
22
  const PERMISSIONS_TABLE = {
23
23
  table: 'permissions',
24
24
  entityType: enum_1.AuditTrailEntityType.PERMISSION,
25
+ // A permissions change shows in the history of the user it belongs to.
26
+ related: { entityType: enum_1.AuditTrailEntityType.USER, column: 'user_id' },
25
27
  };
26
28
  /**
27
29
  * The `permissions` table. Rows are created by the `on_auth_user_created` trigger and deleted
@@ -1,7 +1,7 @@
1
1
  import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { ProjectEntity, ProjectSummary } from '@managemint-solutions/entities/projects';
3
3
  import type { AuthContext } from '../auth';
4
- import { type AuditResource } from '../audit';
4
+ import { EntityAuditTrail, type AuditResource } from '../audit';
5
5
  import type { TypedSupabaseClient } from '../client';
6
6
  import type { CreateProjectDto, GetProjectsDto, UpdateProjectDto } from './dto';
7
7
  export * from './dto';
@@ -9,6 +9,8 @@ export declare class ProjectsResource {
9
9
  private readonly supabase;
10
10
  private readonly auth;
11
11
  private readonly table;
12
+ /** This record's history - its own rows and the rows that hang off it. */
13
+ readonly audit: EntityAuditTrail;
12
14
  constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
13
15
  list(query: GetProjectsDto): Promise<MmsList<ProjectSummary>>;
14
16
  getById(projectId: string): Promise<ProjectEntity>;
@@ -79,6 +79,8 @@ class ProjectsResource {
79
79
  supabase;
80
80
  auth;
81
81
  table;
82
+ /** This record's history - its own rows and the rows that hang off it. */
83
+ audit;
82
84
  constructor(supabase, auth, audit) {
83
85
  this.supabase = supabase;
84
86
  this.auth = auth;
@@ -86,6 +88,7 @@ class ProjectsResource {
86
88
  table: 'projects',
87
89
  entityType: enum_1.AuditTrailEntityType.PROJECTS,
88
90
  });
91
+ this.audit = new audit_1.EntityAuditTrail(supabase, auth, enum_1.AuditTrailEntityType.PROJECTS);
89
92
  }
90
93
  async list(query) {
91
94
  const page = query.page;
@@ -119,6 +119,13 @@ export declare class SupabaseServiceClient {
119
119
  user_id: string;
120
120
  organization_id: string;
121
121
  } | null>;
122
+ /**
123
+ * Deletes every audit row written before `cutoff` and says how many went - the daily
124
+ * retention cron. Every tenant at once: the period is the same for all of them. Batched by
125
+ * the internal identity, oldest first, so no single statement holds a long lock, and each
126
+ * delete is a range rather than a list of ids.
127
+ */
128
+ purgeAuditLogsBefore(cutoff: string): Promise<number>;
122
129
  /**
123
130
  * A ledger row by id alone. Also unscoped by necessity: a `charge.success` carries the
124
131
  * transaction id in its metadata, and the row is what says which tenant it belongs to.
@@ -49,7 +49,7 @@ class SupabaseOrganizationScope {
49
49
  this.leaveEntitlements = new leave_1.LeaveEntitlementsResource(supabase, actor, this.audit);
50
50
  this.leaveRequests = new leave_1.LeaveRequestsResource(supabase, actor, this.audit);
51
51
  this.organizations = new organizations_1.OrganizationsResource(supabase, actor, this.audit);
52
- this.paymentMethod = new organization_payment_methods_1.OrganizationPaymentMethodsResource(supabase, actor, this.audit);
52
+ this.paymentMethod = new organization_payment_methods_1.OrganizationPaymentMethodsResource(supabase, actor);
53
53
  this.permissions = new permissions_1.PermissionsResource(supabase, actor, this.audit);
54
54
  this.users = new users_1.UsersResource(supabase, actor, this.audit);
55
55
  }
@@ -209,6 +209,40 @@ class SupabaseServiceClient {
209
209
  throw (0, errors_1.mapPostgrestError)(error);
210
210
  return (data ?? null);
211
211
  }
212
+ /**
213
+ * Deletes every audit row written before `cutoff` and says how many went - the daily
214
+ * retention cron. Every tenant at once: the period is the same for all of them. Batched by
215
+ * the internal identity, oldest first, so no single statement holds a long lock, and each
216
+ * delete is a range rather than a list of ids.
217
+ */
218
+ async purgeAuditLogsBefore(cutoff) {
219
+ // Supabase's default `max_rows`: a larger limit would come back short and end the loop early.
220
+ const batchSize = 1000;
221
+ let deleted = 0;
222
+ for (;;) {
223
+ const { data, error } = await this.supabase
224
+ .from('audit_logs')
225
+ .select('id')
226
+ .lt('created_at', cutoff)
227
+ .order('id', { ascending: true })
228
+ .limit(batchSize);
229
+ if (error)
230
+ throw (0, errors_1.mapPostgrestError)(error);
231
+ const batch = (data ?? []);
232
+ if (batch.length === 0)
233
+ return deleted;
234
+ const { error: deleteError } = await this.supabase
235
+ .from('audit_logs')
236
+ .delete()
237
+ .lt('created_at', cutoff)
238
+ .lte('id', batch[batch.length - 1].id);
239
+ if (deleteError)
240
+ throw (0, errors_1.mapPostgrestError)(deleteError);
241
+ deleted += batch.length;
242
+ if (batch.length < batchSize)
243
+ return deleted;
244
+ }
245
+ }
212
246
  /**
213
247
  * A ledger row by id alone. Also unscoped by necessity: a `charge.success` carries the
214
248
  * transaction id in its metadata, and the row is what says which tenant it belongs to.
@@ -11,6 +11,11 @@ export declare class SupportingFilesResource {
11
11
  private readonly table;
12
12
  constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
13
13
  getPresignedUrl(supportingFileId: string): Promise<SupportingFilePresignedUrl>;
14
+ /** The record a file hangs off - what the api checks the caller can reach before a download or delete. */
15
+ getParent(supportingFileId: string): Promise<{
16
+ entity_type: SupportingFilesEntityTypes;
17
+ entity_id: string;
18
+ }>;
14
19
  list(entity: SupportingFilesEntityTypes, entityId: string): Promise<SupportingFileEntity[]>;
15
20
  /**
16
21
  * Where the caller puts each file: one signed upload URL per name, in request order, valid
@@ -76,6 +76,18 @@ class SupportingFilesResource {
76
76
  throw (0, errors_1.mapStorageError)(signedError);
77
77
  return { url: signed.signedUrl, ttl: SIGNED_URL_TTL_SECONDS };
78
78
  }
79
+ /** The record a file hangs off - what the api checks the caller can reach before a download or delete. */
80
+ async getParent(supportingFileId) {
81
+ const { data, error } = await this.supabase
82
+ .from('supporting_files')
83
+ .select('entity_type, entity_id')
84
+ .eq('mms_id', supportingFileId)
85
+ .eq('organization_id', this.auth.organization_id)
86
+ .single();
87
+ if (error)
88
+ throw (0, errors_1.mapPostgrestError)(error);
89
+ return data;
90
+ }
79
91
  async list(entity, entityId) {
80
92
  const { data, error } = await this.supabase
81
93
  .from('supporting_files')
@@ -1,16 +1,18 @@
1
1
  import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { TaskEntity, TaskSummary } from '@managemint-solutions/entities/tasks';
3
3
  import type { AuthContext } from '../auth';
4
- import { type AuditResource } from '../audit';
4
+ import { EntityAuditTrail, type AuditResource } from '../audit';
5
5
  import type { TypedSupabaseClient } from '../client';
6
6
  import type { AddTaskLinkDto, CreateTaskDto, GetTasksDto, UpdateTaskDto } from './dto';
7
7
  export * from './dto';
8
8
  export declare class TasksResource {
9
9
  private readonly supabase;
10
10
  private readonly auth;
11
- private readonly audit;
11
+ private readonly auditLog;
12
12
  private readonly table;
13
- constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
13
+ /** This record's history - its own rows and the rows that hang off it. */
14
+ readonly audit: EntityAuditTrail;
15
+ constructor(supabase: TypedSupabaseClient, auth: AuthContext, auditLog: AuditResource);
14
16
  list(query: GetTasksDto): Promise<MmsList<TaskSummary>>;
15
17
  getById(taskId: string): Promise<TaskEntity>;
16
18
  create(input: CreateTaskDto): Promise<TaskEntity>;
@@ -43,11 +45,12 @@ export declare class TasksResource {
43
45
  private checkLabels;
44
46
  /**
45
47
  * Makes `labelIds` the task's whole label set. The join rows carry no audit of their own: the
46
- * change is one UPDATE entry on the task, `labels: { old: [names], new: [names] }`, recorded
47
- * only when the set actually moved - on a new task that entry is the record of the initial
48
- * pinning. Only the difference is written, the new labels inserted before the dropped ones are
49
- * deleted, so should the trigger still refuse one (a label deleted after `checkLabels`) the
50
- * task's existing labels are untouched, and a label kept across an edit keeps its place.
48
+ * change is an UPDATE entry on the task, `labels: { old: [names], new: [names] }`, recorded
49
+ * only when the set actually moved - inside a request's audit batch it folds into the task's
50
+ * own entry for that save (`withAuditBatch`). Only the difference is written, the new labels
51
+ * inserted before the dropped ones are deleted, so should the trigger still refuse one (a label
52
+ * deleted after `checkLabels`) the task's existing labels are untouched, and a label kept
53
+ * across an edit keeps its place.
51
54
  */
52
55
  private setLabels;
53
56
  }
@@ -124,16 +124,19 @@ const linkedTasks = (rows, linkType) => rows
124
124
  class TasksResource {
125
125
  supabase;
126
126
  auth;
127
- audit;
127
+ auditLog;
128
128
  table;
129
- constructor(supabase, auth, audit) {
129
+ /** This record's history - its own rows and the rows that hang off it. */
130
+ audit;
131
+ constructor(supabase, auth, auditLog) {
130
132
  this.supabase = supabase;
131
133
  this.auth = auth;
132
- this.audit = audit;
133
- this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
134
+ this.auditLog = auditLog;
135
+ this.table = (0, audit_1.auditedTable)(supabase, auth, auditLog, {
134
136
  table: 'tasks',
135
137
  entityType: enum_3.AuditTrailEntityType.TASKS,
136
138
  });
139
+ this.audit = new audit_1.EntityAuditTrail(supabase, auth, enum_3.AuditTrailEntityType.TASKS);
137
140
  }
138
141
  async list(query) {
139
142
  const page = query.page;
@@ -356,7 +359,7 @@ class TasksResource {
356
359
  throw (0, errors_1.mapPostgrestError)(error);
357
360
  }
358
361
  const role = input.link_type === enum_1.TaskLinkType.CHILD ? 'subtask' : 'related';
359
- await this.audit.record({
362
+ await this.auditLog.record({
360
363
  action: enum_3.AuditTrailAction.UPDATE,
361
364
  entityType: enum_3.AuditTrailEntityType.TASKS,
362
365
  entityId: taskId,
@@ -384,7 +387,7 @@ class TasksResource {
384
387
  }
385
388
  const madeHere = row.task_id === taskId;
386
389
  const role = row.link_type === enum_1.TaskLinkType.RELATED ? 'related' : madeHere ? 'subtask' : 'parent';
387
- await this.audit.record({
390
+ await this.auditLog.record({
388
391
  action: enum_3.AuditTrailAction.UPDATE,
389
392
  entityType: enum_3.AuditTrailEntityType.TASKS,
390
393
  entityId: taskId,
@@ -421,11 +424,12 @@ class TasksResource {
421
424
  }
422
425
  /**
423
426
  * Makes `labelIds` the task's whole label set. The join rows carry no audit of their own: the
424
- * change is one UPDATE entry on the task, `labels: { old: [names], new: [names] }`, recorded
425
- * only when the set actually moved - on a new task that entry is the record of the initial
426
- * pinning. Only the difference is written, the new labels inserted before the dropped ones are
427
- * deleted, so should the trigger still refuse one (a label deleted after `checkLabels`) the
428
- * task's existing labels are untouched, and a label kept across an edit keeps its place.
427
+ * change is an UPDATE entry on the task, `labels: { old: [names], new: [names] }`, recorded
428
+ * only when the set actually moved - inside a request's audit batch it folds into the task's
429
+ * own entry for that save (`withAuditBatch`). Only the difference is written, the new labels
430
+ * inserted before the dropped ones are deleted, so should the trigger still refuse one (a label
431
+ * deleted after `checkLabels`) the task's existing labels are untouched, and a label kept
432
+ * across an edit keeps its place.
429
433
  */
430
434
  async setLabels(taskId, labelIds) {
431
435
  const { data: current, error: readError } = await this.supabase
@@ -467,7 +471,7 @@ class TasksResource {
467
471
  if (error)
468
472
  throw (0, errors_1.mapPostgrestError)(error);
469
473
  }
470
- await this.audit.record({
474
+ await this.auditLog.record({
471
475
  action: enum_3.AuditTrailAction.UPDATE,
472
476
  entityType: enum_3.AuditTrailEntityType.TASKS,
473
477
  entityId: taskId,
@@ -59,6 +59,7 @@ class TimesheetEntriesResource {
59
59
  this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
60
60
  table: 'timesheet_entries',
61
61
  entityType: enum_1.AuditTrailEntityType.TIMESHEET_ENTRIES,
62
+ related: { entityType: enum_1.AuditTrailEntityType.TIMESHEETS, column: 'timesheet_id' },
62
63
  });
63
64
  }
64
65
  async getById(timesheetEntryId) {
@@ -2,7 +2,7 @@ import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { TimesheetEntity, TimesheetSummary } from '@managemint-solutions/entities/timesheets';
3
3
  import { TimesheetReviewDecision, TimesheetStatus } from '@managemint-solutions/entities/timesheets/enum';
4
4
  import type { AuthContext } from '../auth';
5
- import { type AuditResource } from '../audit';
5
+ import { EntityAuditTrail, type AuditResource } from '../audit';
6
6
  import type { TypedSupabaseClient } from '../client';
7
7
  import type { GetTimesheetsDto } from './dto';
8
8
  /** What the entry path needs of the week an entry falls in: its id and whether it is still open. */
@@ -29,6 +29,8 @@ export declare class TimesheetsResource {
29
29
  private readonly supabase;
30
30
  private readonly auth;
31
31
  private readonly table;
32
+ /** This record's history - its own rows and the rows that hang off it. */
33
+ readonly audit: EntityAuditTrail;
32
34
  constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
33
35
  list(query: GetTimesheetsDto, options?: ListOptions): Promise<MmsList<TimesheetSummary>>;
34
36
  /** The week with its entries, by date then start time. */
@@ -27,6 +27,8 @@ class TimesheetsResource {
27
27
  supabase;
28
28
  auth;
29
29
  table;
30
+ /** This record's history - its own rows and the rows that hang off it. */
31
+ audit;
30
32
  constructor(supabase, auth, audit) {
31
33
  this.supabase = supabase;
32
34
  this.auth = auth;
@@ -34,6 +36,7 @@ class TimesheetsResource {
34
36
  table: 'timesheets',
35
37
  entityType: enum_1.AuditTrailEntityType.TIMESHEETS,
36
38
  });
39
+ this.audit = new audit_1.EntityAuditTrail(supabase, auth, enum_1.AuditTrailEntityType.TIMESHEETS);
37
40
  }
38
41
  async list(query, options = {}) {
39
42
  const page = query.page;
@@ -1,6 +1,6 @@
1
1
  import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { UserIdentity, UserSummary } from '@managemint-solutions/entities/users';
3
- import { type AuditActor, type AuditResource, type AuditRow } from '../audit';
3
+ import { type AuditActor, type AuditResource, type AuditRow, EntityAuditTrail } from '../audit';
4
4
  import type { TypedSupabaseClient } from '../client';
5
5
  import type { GetUsersQueryDto } from './dto';
6
6
  export * from './dto';
@@ -24,6 +24,8 @@ export declare class UsersResource {
24
24
  private readonly supabase;
25
25
  private readonly auth;
26
26
  private readonly table;
27
+ /** This record's history - its own rows and the rows that hang off it. */
28
+ readonly audit: EntityAuditTrail;
27
29
  constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
28
30
  list(query: GetUsersQueryDto): Promise<MmsList<UserSummary>>;
29
31
  getRow(userId: string): Promise<UserRow>;