@managemint-solutions/sdk 0.55.0 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +209 -7
  2. package/dist/audit/document-changes.d.ts +39 -0
  3. package/dist/audit/document-changes.js +332 -0
  4. package/dist/audit/index.d.ts +50 -2
  5. package/dist/audit/index.js +87 -44
  6. package/dist/client.d.ts +6 -0
  7. package/dist/client.js +9 -0
  8. package/dist/clients/dto.d.ts +18 -3
  9. package/dist/clients/dto.js +72 -2
  10. package/dist/clients/index.js +16 -0
  11. package/dist/documents/content.d.ts +39 -0
  12. package/dist/documents/content.js +443 -0
  13. package/dist/documents/document-templates.d.ts +37 -0
  14. package/dist/documents/document-templates.js +143 -0
  15. package/dist/documents/documents.d.ts +154 -0
  16. package/dist/documents/documents.js +330 -0
  17. package/dist/documents/dto.d.ts +125 -0
  18. package/dist/documents/dto.js +530 -0
  19. package/dist/documents/index.d.ts +4 -0
  20. package/dist/documents/index.js +24 -0
  21. package/dist/documents/render/html.d.ts +64 -0
  22. package/dist/documents/render/html.js +399 -0
  23. package/dist/documents/render/index.d.ts +2 -0
  24. package/dist/documents/render/index.js +23 -0
  25. package/dist/documents/render/values.d.ts +86 -0
  26. package/dist/documents/render/values.js +142 -0
  27. package/dist/index.d.ts +8 -1
  28. package/dist/index.js +27 -3
  29. package/dist/leads/index.js +1 -3
  30. package/dist/notes/index.d.ts +2 -2
  31. package/dist/notes/index.js +6 -10
  32. package/dist/notifications/index.d.ts +6 -0
  33. package/dist/notifications/index.js +16 -0
  34. package/dist/organization-payment-methods/dto.d.ts +10 -0
  35. package/dist/organization-payment-methods/dto.js +56 -0
  36. package/dist/organization-payment-methods/index.d.ts +8 -1
  37. package/dist/organization-payment-methods/index.js +23 -0
  38. package/dist/payments/index.d.ts +67 -0
  39. package/dist/payments/index.js +137 -0
  40. package/dist/permissions/dto.d.ts +4 -0
  41. package/dist/permissions/dto.js +20 -8
  42. package/dist/service-client/index.d.ts +35 -0
  43. package/dist/service-client/index.js +64 -0
  44. package/dist/staff-client.d.ts +10 -0
  45. package/dist/staff-client.js +15 -2
  46. package/dist/supporting-files/index.d.ts +12 -4
  47. package/dist/supporting-files/index.js +25 -25
  48. package/dist/supporting-files/paths.d.ts +2 -0
  49. package/dist/supporting-files/paths.js +3 -1
  50. package/dist/validators.d.ts +6 -0
  51. package/dist/validators.js +14 -1
  52. package/dist/visibility/dto.d.ts +15 -0
  53. package/dist/visibility/dto.js +68 -0
  54. package/dist/visibility/index.d.ts +59 -0
  55. package/dist/visibility/index.js +165 -0
  56. package/package.json +6 -2
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PaymentsResource = void 0;
4
+ const enum_1 = require("@managemint-solutions/entities/payments/enum");
5
+ const enum_2 = require("@managemint-solutions/entities/audit-trail/enum");
6
+ const audit_1 = require("../audit");
7
+ const errors_1 = require("../errors");
8
+ const tenancy_1 = require("../tenancy");
9
+ const transforms_1 = require("../transforms");
10
+ const PAYMENT_SELECT = `
11
+ mms_id,
12
+ created_at,
13
+ created_by:user_profiles!created_by ${transforms_1.USER_PROFILE_EMBED},
14
+ direction,
15
+ status,
16
+ amount_minor,
17
+ currency,
18
+ method,
19
+ reference,
20
+ payer_email,
21
+ paid_at
22
+ `;
23
+ /**
24
+ * `public.payments`: money received for a record, keyed by `entity_type` + `entity_id` like the
25
+ * visibility table. Read on the caller's client (RLS shows a payment while its record is
26
+ * readable); written on the service client's organization scope alone - members hold only a
27
+ * SELECT grant. A checkout starts as an `open` row (one per record, by a partial unique index)
28
+ * and is settled to `paid`. Each write is audited and, through its `entity_type`/`entity_id`,
29
+ * filed under the record it pays for.
30
+ */
31
+ class PaymentsResource {
32
+ supabase;
33
+ actor;
34
+ table;
35
+ constructor(supabase, actor, audit) {
36
+ this.supabase = supabase;
37
+ this.actor = actor;
38
+ this.table = (0, audit_1.auditedTable)(supabase, actor, audit, {
39
+ table: 'payments',
40
+ entityType: enum_2.AuditTrailEntityType.PAYMENTS,
41
+ });
42
+ }
43
+ /** The money received for one record, oldest first - paid rows only. */
44
+ async list(entityType, entityId) {
45
+ const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('payments').select(PAYMENT_SELECT), this.actor)
46
+ .eq('entity_type', entityType)
47
+ .eq('entity_id', entityId)
48
+ .eq('status', enum_1.PaymentStatus.PAID)
49
+ .order('paid_at', { ascending: true });
50
+ if (error)
51
+ throw (0, errors_1.mapPostgrestError)(error);
52
+ return data;
53
+ }
54
+ /** How many paid payments each of `entityIds` has - one query for a page of a list. */
55
+ async paidCounts(entityType, entityIds) {
56
+ if (entityIds.length === 0)
57
+ return {};
58
+ const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('payments').select('entity_id'), this.actor)
59
+ .eq('entity_type', entityType)
60
+ .eq('status', enum_1.PaymentStatus.PAID)
61
+ .in('entity_id', entityIds);
62
+ if (error)
63
+ throw (0, errors_1.mapPostgrestError)(error);
64
+ const counts = {};
65
+ for (const { entity_id } of (data ?? [])) {
66
+ counts[entity_id] = (counts[entity_id] ?? 0) + 1;
67
+ }
68
+ return counts;
69
+ }
70
+ /**
71
+ * The ids of the organization's records of one kind with a settled payment, each once - the
72
+ * documents list's payment filter reads it before the page.
73
+ */
74
+ async paidEntityIds(entityType) {
75
+ const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('payments').select('entity_id'), this.actor)
76
+ .eq('entity_type', entityType)
77
+ .eq('status', enum_1.PaymentStatus.PAID);
78
+ if (error)
79
+ throw (0, errors_1.mapPostgrestError)(error);
80
+ return [...new Set((data ?? []).map((row) => row.entity_id))];
81
+ }
82
+ /** The record's open checkout, if one is under way. */
83
+ async findOpen(entityType, entityId) {
84
+ const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('payments').select(`${PAYMENT_SELECT}, access_code`), this.actor)
85
+ .eq('entity_type', entityType)
86
+ .eq('entity_id', entityId)
87
+ .eq('status', enum_1.PaymentStatus.OPEN)
88
+ .maybeSingle();
89
+ if (error)
90
+ throw (0, errors_1.mapPostgrestError)(error);
91
+ return (data ?? null);
92
+ }
93
+ /** A payment by the provider's reference (unique), whatever its status. */
94
+ async findByReference(reference) {
95
+ const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('payments').select(PAYMENT_SELECT), this.actor)
96
+ .eq('reference', reference)
97
+ .maybeSingle();
98
+ if (error)
99
+ throw (0, errors_1.mapPostgrestError)(error);
100
+ return (data ?? null);
101
+ }
102
+ /**
103
+ * Records one payment, by the actor (null for the system). A Paystack reference recorded
104
+ * already, or a second open checkout for the record, is refused by a unique index - 409 with
105
+ * `code` 23505.
106
+ */
107
+ async create(row) {
108
+ await this.table.insert({
109
+ ...row,
110
+ organization_id: (0, tenancy_1.requireOrganization)(this.actor),
111
+ created_by: this.actor.mms_id,
112
+ created_at: new Date().toISOString(),
113
+ });
114
+ }
115
+ /**
116
+ * Removes a payment marked by hand (EFT) from the record it pays for. False when it is not one -
117
+ * a Paystack payment is money Paystack holds, refunded there, never removed here.
118
+ */
119
+ async removeManual(paymentId, entityType, entityId) {
120
+ const removed = await this.table.removeWhere(paymentId, (query) => query.eq('entity_type', entityType).eq('entity_id', entityId).eq('method', enum_1.PaymentMethod.EFT));
121
+ return removed !== null;
122
+ }
123
+ /** Settles an open checkout. False when it is no longer open (settled or replaced first). */
124
+ async settle(paymentId, row) {
125
+ const settled = await this.table.updateWhere(paymentId, (query) => query.eq('status', enum_1.PaymentStatus.OPEN), {
126
+ ...row,
127
+ status: enum_1.PaymentStatus.PAID,
128
+ });
129
+ return settled !== null;
130
+ }
131
+ /** Drops an open checkout Paystack says failed, so a new one can open. False when it moved on. */
132
+ async removeOpen(paymentId) {
133
+ const removed = await this.table.removeWhere(paymentId, (query) => query.eq('status', enum_1.PaymentStatus.OPEN));
134
+ return removed !== null;
135
+ }
136
+ }
137
+ exports.PaymentsResource = PaymentsResource;
@@ -20,6 +20,10 @@ export declare class UpdatePermissionsDto implements UpdatePermissionsDtoType {
20
20
  readonly read_timesheets: boolean;
21
21
  readonly update_timesheets: boolean;
22
22
  readonly delete_timesheets: boolean;
23
+ readonly create_documents: boolean;
24
+ readonly read_documents: boolean;
25
+ readonly update_documents: boolean;
26
+ readonly delete_documents: boolean;
23
27
  readonly create_leave: boolean;
24
28
  readonly read_leave: boolean;
25
29
  readonly update_leave: boolean;
@@ -42,14 +42,10 @@ class UpdatePermissionsDto {
42
42
  read_timesheets;
43
43
  update_timesheets;
44
44
  delete_timesheets;
45
- // @IsBoolean({ message: 'Create Documents must be a boolean' })
46
- // readonly create_documents!: boolean;
47
- // @IsBoolean({ message: 'Read Documents must be a boolean' })
48
- // readonly read_documents!: boolean;
49
- // @IsBoolean({ message: 'Update Documents must be a boolean' })
50
- // readonly update_documents!: boolean;
51
- // @IsBoolean({ message: 'Delete Documents must be a boolean' })
52
- // readonly delete_documents!: boolean;
45
+ create_documents;
46
+ read_documents;
47
+ update_documents;
48
+ delete_documents;
53
49
  create_leave;
54
50
  read_leave;
55
51
  update_leave;
@@ -192,6 +188,22 @@ __decorate([
192
188
  (0, class_validator_1.IsBoolean)({ message: 'Delete Timesheets must be a boolean' }),
193
189
  __metadata("design:type", Boolean)
194
190
  ], UpdatePermissionsDto.prototype, "delete_timesheets", void 0);
191
+ __decorate([
192
+ (0, class_validator_1.IsBoolean)({ message: 'Create Documents must be a boolean' }),
193
+ __metadata("design:type", Boolean)
194
+ ], UpdatePermissionsDto.prototype, "create_documents", void 0);
195
+ __decorate([
196
+ (0, class_validator_1.IsBoolean)({ message: 'Read Documents must be a boolean' }),
197
+ __metadata("design:type", Boolean)
198
+ ], UpdatePermissionsDto.prototype, "read_documents", void 0);
199
+ __decorate([
200
+ (0, class_validator_1.IsBoolean)({ message: 'Update Documents must be a boolean' }),
201
+ __metadata("design:type", Boolean)
202
+ ], UpdatePermissionsDto.prototype, "update_documents", void 0);
203
+ __decorate([
204
+ (0, class_validator_1.IsBoolean)({ message: 'Delete Documents must be a boolean' }),
205
+ __metadata("design:type", Boolean)
206
+ ], UpdatePermissionsDto.prototype, "delete_documents", void 0);
195
207
  __decorate([
196
208
  (0, class_validator_1.IsBoolean)({ message: 'Create Leave must be a boolean' }),
197
209
  __metadata("design:type", Boolean)
@@ -4,6 +4,7 @@ import { BillingResource } from '../billing';
4
4
  import { InvoicesResource } from '../billing/invoices';
5
5
  import { ClientsResource } from '../clients';
6
6
  import { ConfigsResource } from '../configs';
7
+ import { DocumentsResource } from '../documents';
7
8
  import { EmulationHandshakesResource } from '../emulation-handshakes';
8
9
  import { FeatureRequestsResource } from '../feature-requests';
9
10
  import { LeaveEntitlementsResource, LeaveRequestsResource, LeaveTypesResource } from '../leave';
@@ -11,10 +12,13 @@ import { NotificationsOutboxResource } from '../notifications';
11
12
  import { OrganizationInvitesResource } from '../organization-invites';
12
13
  import { OrganizationPaymentMethodsResource, type PaymentMethodLookupColumn } from '../organization-payment-methods';
13
14
  import { OrganizationsResource, OrganizationSignupResource } from '../organizations';
15
+ import { PaymentsResource } from '../payments';
14
16
  import { PermissionsResource } from '../permissions';
15
17
  import { StaffPermissionsResource, StaffResource } from '../staff';
18
+ import { SupportingFilesResource } from '../supporting-files';
16
19
  import { UserApiCredentialsResource } from '../user-api-credentials';
17
20
  import { UsersResource } from '../users';
21
+ import { VisibilityResource } from '../visibility';
18
22
  import { WebhookEventsResource } from '../webhook-events';
19
23
  export type SupabaseServiceClientConfig = {
20
24
  url: string;
@@ -33,6 +37,13 @@ export declare class SupabaseOrganizationScope {
33
37
  readonly actor: AuditActor;
34
38
  readonly audit: AuditResource;
35
39
  readonly billing: BillingResource;
40
+ /**
41
+ * For the public pay page, which has no caller (the document behind a pay link, and its PDF),
42
+ * the Paystack webhook, and the daily send cap, which counts private documents too.
43
+ */
44
+ readonly documents: DocumentsResource;
45
+ /** Money received: written here alone, by the Paystack webhook. */
46
+ readonly payments: PaymentsResource;
36
47
  readonly invoices: InvoicesResource;
37
48
  readonly invites: OrganizationInvitesResource;
38
49
  readonly leaveTypes: LeaveTypesResource;
@@ -41,7 +52,11 @@ export declare class SupabaseOrganizationScope {
41
52
  readonly organizations: OrganizationsResource;
42
53
  readonly paymentMethod: OrganizationPaymentMethodsResource;
43
54
  readonly permissions: PermissionsResource;
55
+ /** For the admin api's staff downloads: Storage's read policy names no staff, the service role passes it. */
56
+ readonly supportingFiles: SupportingFilesResource;
44
57
  readonly users: UsersResource;
58
+ /** Who private records are shared with: listed and written here, after the api's own checks. */
59
+ readonly visibility: VisibilityResource;
45
60
  constructor(supabase: TypedSupabaseClient, actor: AuditActor, onAuditError?: AuditErrorHandler);
46
61
  /**
47
62
  * An audited table bound to this organization and actor - the shape the api hand-rolls today
@@ -150,6 +165,26 @@ export declare class SupabaseServiceClient {
150
165
  user_id: string;
151
166
  organization_id: string;
152
167
  } | null>;
168
+ /**
169
+ * The recurring parents due a draft: not paused, not ended, `repeat_next_at` on or before
170
+ * `onOrBefore` (YYYY-MM-DD). Across every organization, like the leave rollover's tenants - the
171
+ * cron then works inside each row's own organization.
172
+ */
173
+ documentsDueForDraft(onOrBefore: string): Promise<{
174
+ mms_id: string;
175
+ organization_id: string;
176
+ }[]>;
177
+ /** The drafts due to issue: `issue_at` on or before `onOrBefore`, across every organization. */
178
+ documentsDueToIssue(onOrBefore: string): Promise<{
179
+ mms_id: string;
180
+ organization_id: string;
181
+ }[]>;
182
+ /**
183
+ * Which organization a document belongs to, by its id alone. Unscoped by necessity, like the
184
+ * calendar feed: a public pay link (whose signature the api has checked) names only the
185
+ * document, and the row is what says which tenant it belongs to.
186
+ */
187
+ findDocumentOrganization(documentId: string): Promise<string | null>;
153
188
  /**
154
189
  * Deletes every audit row written before `cutoff` and says how many went - the daily
155
190
  * retention cron. Every tenant at once: the period is the same for all of them. Batched by
@@ -4,10 +4,12 @@ exports.createSupabaseServiceClient = exports.SupabaseServiceClient = exports.Su
4
4
  const auth_1 = require("../auth");
5
5
  const audit_1 = require("../audit");
6
6
  const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
7
+ const enum_2 = require("@managemint-solutions/entities/documents/enum");
7
8
  const billing_1 = require("../billing");
8
9
  const invoices_1 = require("../billing/invoices");
9
10
  const clients_1 = require("../clients");
10
11
  const configs_1 = require("../configs");
12
+ const documents_1 = require("../documents");
11
13
  const emulation_handshakes_1 = require("../emulation-handshakes");
12
14
  const feature_requests_1 = require("../feature-requests");
13
15
  const leave_1 = require("../leave");
@@ -16,10 +18,13 @@ const errors_1 = require("../errors");
16
18
  const organization_invites_1 = require("../organization-invites");
17
19
  const organization_payment_methods_1 = require("../organization-payment-methods");
18
20
  const organizations_1 = require("../organizations");
21
+ const payments_1 = require("../payments");
19
22
  const permissions_1 = require("../permissions");
20
23
  const staff_1 = require("../staff");
24
+ const supporting_files_1 = require("../supporting-files");
21
25
  const user_api_credentials_1 = require("../user-api-credentials");
22
26
  const users_1 = require("../users");
27
+ const visibility_1 = require("../visibility");
23
28
  const webhook_events_1 = require("../webhook-events");
24
29
  /**
25
30
  * The organization-scoped half of the service client. Everything here is attributed to one
@@ -31,6 +36,13 @@ class SupabaseOrganizationScope {
31
36
  actor;
32
37
  audit;
33
38
  billing;
39
+ /**
40
+ * For the public pay page, which has no caller (the document behind a pay link, and its PDF),
41
+ * the Paystack webhook, and the daily send cap, which counts private documents too.
42
+ */
43
+ documents;
44
+ /** Money received: written here alone, by the Paystack webhook. */
45
+ payments;
34
46
  invoices;
35
47
  invites;
36
48
  leaveTypes;
@@ -39,12 +51,18 @@ class SupabaseOrganizationScope {
39
51
  organizations;
40
52
  paymentMethod;
41
53
  permissions;
54
+ /** For the admin api's staff downloads: Storage's read policy names no staff, the service role passes it. */
55
+ supportingFiles;
42
56
  users;
57
+ /** Who private records are shared with: listed and written here, after the api's own checks. */
58
+ visibility;
43
59
  constructor(supabase, actor, onAuditError) {
44
60
  this.supabase = supabase;
45
61
  this.actor = actor;
46
62
  this.audit = new audit_1.AuditResource(supabase, actor, onAuditError);
47
63
  this.billing = new billing_1.BillingResource(supabase, actor, this.audit);
64
+ this.documents = new documents_1.DocumentsResource(supabase, actor, this.audit);
65
+ this.payments = new payments_1.PaymentsResource(supabase, actor, this.audit);
48
66
  this.invoices = new invoices_1.InvoicesResource(supabase, actor, this.audit);
49
67
  this.invites = new organization_invites_1.OrganizationInvitesResource(supabase, actor);
50
68
  // The leave-year rollover writes next period's entitlements as the system (`mms_id: null`).
@@ -54,7 +72,9 @@ class SupabaseOrganizationScope {
54
72
  this.organizations = new organizations_1.OrganizationsResource(supabase, actor, this.audit);
55
73
  this.paymentMethod = new organization_payment_methods_1.OrganizationPaymentMethodsResource(supabase, actor);
56
74
  this.permissions = new permissions_1.PermissionsResource(supabase, actor, this.audit);
75
+ this.supportingFiles = new supporting_files_1.SupportingFilesResource(supabase, actor, this.audit);
57
76
  this.users = new users_1.UsersResource(supabase, actor, this.audit);
77
+ this.visibility = new visibility_1.VisibilityResource(supabase, actor, this.audit);
58
78
  }
59
79
  /**
60
80
  * An audited table bound to this organization and actor - the shape the api hand-rolls today
@@ -262,6 +282,50 @@ class SupabaseServiceClient {
262
282
  throw (0, errors_1.mapPostgrestError)(error);
263
283
  return (data ?? null);
264
284
  }
285
+ /**
286
+ * The recurring parents due a draft: not paused, not ended, `repeat_next_at` on or before
287
+ * `onOrBefore` (YYYY-MM-DD). Across every organization, like the leave rollover's tenants - the
288
+ * cron then works inside each row's own organization.
289
+ */
290
+ async documentsDueForDraft(onOrBefore) {
291
+ const { data, error } = await this.supabase
292
+ .from('documents')
293
+ .select('mms_id, organization_id')
294
+ .not('repeat', 'is', null)
295
+ .eq('repeat_paused', false)
296
+ .eq('status', enum_2.DocumentStatus.DRAFT)
297
+ .lte('repeat_next_at', onOrBefore);
298
+ if (error)
299
+ throw (0, errors_1.mapPostgrestError)(error);
300
+ return (data ?? []);
301
+ }
302
+ /** The drafts due to issue: `issue_at` on or before `onOrBefore`, across every organization. */
303
+ async documentsDueToIssue(onOrBefore) {
304
+ const { data, error } = await this.supabase
305
+ .from('documents')
306
+ .select('mms_id, organization_id')
307
+ .eq('status', enum_2.DocumentStatus.DRAFT)
308
+ .is('repeat', null)
309
+ .lte('issue_at', onOrBefore);
310
+ if (error)
311
+ throw (0, errors_1.mapPostgrestError)(error);
312
+ return (data ?? []);
313
+ }
314
+ /**
315
+ * Which organization a document belongs to, by its id alone. Unscoped by necessity, like the
316
+ * calendar feed: a public pay link (whose signature the api has checked) names only the
317
+ * document, and the row is what says which tenant it belongs to.
318
+ */
319
+ async findDocumentOrganization(documentId) {
320
+ const { data, error } = await this.supabase
321
+ .from('documents')
322
+ .select('organization_id')
323
+ .eq('mms_id', documentId)
324
+ .maybeSingle();
325
+ if (error)
326
+ throw (0, errors_1.mapPostgrestError)(error);
327
+ return data?.organization_id ?? null;
328
+ }
265
329
  /**
266
330
  * Deletes every audit row written before `cutoff` and says how many went - the daily
267
331
  * retention cron. Every tenant at once: the period is the same for all of them. Batched by
@@ -1,11 +1,15 @@
1
1
  import { type StaffContext } from './auth';
2
2
  import { AuditResource } from './audit';
3
+ import { AdditionalDetailsResource } from './additional-details';
3
4
  import type { SupabaseClientConfig, TypedSupabaseClient } from './client';
4
5
  import { ClientsResource } from './clients';
6
+ import { DocumentsResource, DocumentTemplatesResource } from './documents';
5
7
  import { LeadsResource } from './leads';
8
+ import { NotesResource } from './notes';
6
9
  import { OrganizationsResource } from './organizations';
7
10
  import { ProjectsResource } from './projects';
8
11
  import { StaffPermissionsResource, StaffResource } from './staff';
12
+ import { SupportingFilesResource } from './supporting-files';
9
13
  import { TasksResource } from './tasks';
10
14
  import { UserApiCredentialsReads } from './user-api-credentials';
11
15
  import { UsersResource } from './users';
@@ -22,14 +26,20 @@ export declare class SupabaseStaffClient {
22
26
  /** The staff member the token belongs to - the caller, for a service that records who acted. */
23
27
  readonly auth: StaffContext;
24
28
  readonly audit: AuditResource;
29
+ readonly additionalDetails: AdditionalDetailsResource;
25
30
  readonly clients: ClientsResource;
26
31
  readonly leads: LeadsResource;
32
+ readonly notes: NotesResource;
27
33
  readonly projects: ProjectsResource;
28
34
  readonly tasks: TasksResource;
35
+ readonly documentTemplates: DocumentTemplatesResource;
36
+ readonly documents: DocumentsResource;
29
37
  readonly users: UsersResource;
30
38
  readonly organizations: OrganizationsResource;
31
39
  readonly staff: StaffResource;
32
40
  readonly staffPermissions: StaffPermissionsResource;
41
+ /** Lists and parents only: Storage names no staff, so a download is signed on the service client. */
42
+ readonly supportingFiles: SupportingFilesResource;
33
43
  readonly userApiCredentials: UserApiCredentialsReads;
34
44
  constructor({ url, key, accessToken, auth, client }: SupabaseStaffClientConfig);
35
45
  }
@@ -5,10 +5,13 @@ const auth_1 = require("./auth");
5
5
  const audit_1 = require("./audit");
6
6
  const additional_details_1 = require("./additional-details");
7
7
  const clients_1 = require("./clients");
8
+ const documents_1 = require("./documents");
8
9
  const leads_1 = require("./leads");
10
+ const notes_1 = require("./notes");
9
11
  const organizations_1 = require("./organizations");
10
12
  const projects_1 = require("./projects");
11
13
  const staff_1 = require("./staff");
14
+ const supporting_files_1 = require("./supporting-files");
12
15
  const tasks_1 = require("./tasks");
13
16
  const user_api_credentials_1 = require("./user-api-credentials");
14
17
  const users_1 = require("./users");
@@ -22,28 +25,38 @@ class SupabaseStaffClient {
22
25
  /** The staff member the token belongs to - the caller, for a service that records who acted. */
23
26
  auth;
24
27
  audit;
28
+ additionalDetails;
25
29
  clients;
26
30
  leads;
31
+ notes;
27
32
  projects;
28
33
  tasks;
34
+ documentTemplates;
35
+ documents;
29
36
  users;
30
37
  organizations;
31
38
  staff;
32
39
  staffPermissions;
40
+ /** Lists and parents only: Storage names no staff, so a download is signed on the service client. */
41
+ supportingFiles;
33
42
  userApiCredentials;
34
43
  constructor({ url, key, accessToken, auth, client }) {
35
44
  this.supabase = client ?? (0, auth_1.createRawClient)(url, key, accessToken);
36
45
  this.auth = auth;
37
46
  this.audit = new audit_1.AuditResource(this.supabase, auth);
38
47
  this.clients = new clients_1.ClientsResource(this.supabase, auth, this.audit);
39
- const additionalDetails = new additional_details_1.AdditionalDetailsResource(this.supabase, auth, this.audit);
40
- this.leads = new leads_1.LeadsResource(this.supabase, auth, this.audit, additionalDetails);
48
+ this.additionalDetails = new additional_details_1.AdditionalDetailsResource(this.supabase, auth, this.audit);
49
+ this.leads = new leads_1.LeadsResource(this.supabase, auth, this.audit, this.additionalDetails);
50
+ this.notes = new notes_1.NotesResource(this.supabase, auth, this.audit);
41
51
  this.projects = new projects_1.ProjectsResource(this.supabase, auth, this.audit);
42
52
  this.tasks = new tasks_1.TasksResource(this.supabase, auth, this.audit);
53
+ this.documentTemplates = new documents_1.DocumentTemplatesResource(this.supabase, auth, this.audit);
54
+ this.documents = new documents_1.DocumentsResource(this.supabase, auth, this.audit);
43
55
  this.users = new users_1.UsersResource(this.supabase, auth, this.audit);
44
56
  this.organizations = new organizations_1.OrganizationsResource(this.supabase, auth, this.audit);
45
57
  this.staff = new staff_1.StaffResource(this.supabase);
46
58
  this.staffPermissions = new staff_1.StaffPermissionsResource(this.supabase);
59
+ this.supportingFiles = new supporting_files_1.SupportingFilesResource(this.supabase, auth, this.audit);
47
60
  this.userApiCredentials = new user_api_credentials_1.UserApiCredentialsReads(this.supabase);
48
61
  }
49
62
  }
@@ -1,7 +1,6 @@
1
1
  import type { SupportingFileEntity, SupportingFilePresignedUrl, SupportingFileUploadTarget } from '@managemint-solutions/entities/supporting-files';
2
2
  import { SupportingFilesEntityTypes } from '@managemint-solutions/entities/supporting-files/enum';
3
- import type { AuthContext } from '../auth';
4
- import { type AuditResource } from '../audit';
3
+ import { type AuditResource, type ResourceActor } from '../audit';
5
4
  import type { TypedSupabaseClient } from '../client';
6
5
  export * from './dto';
7
6
  export * from './paths';
@@ -9,12 +8,21 @@ export declare class SupportingFilesResource {
9
8
  private readonly supabase;
10
9
  private readonly auth;
11
10
  private readonly table;
12
- constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
11
+ constructor(supabase: TypedSupabaseClient, auth: ResourceActor, audit: AuditResource);
12
+ /**
13
+ * A short-lived download link. Storage checks the bucket's read policy as this client's
14
+ * caller, which names no staff - the admin api signs on the service client's organization
15
+ * scope instead.
16
+ */
13
17
  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. */
18
+ /**
19
+ * The record a file hangs off, and its organization - what the api checks the caller can
20
+ * reach before a download or delete.
21
+ */
15
22
  getParent(supportingFileId: string): Promise<{
16
23
  entity_type: SupportingFilesEntityTypes;
17
24
  entity_id: string;
25
+ organization_id: string;
18
26
  }>;
19
27
  list(entity: SupportingFilesEntityTypes, entityId: string): Promise<SupportingFileEntity[]>;
20
28
  /**
@@ -20,11 +20,10 @@ 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
22
  const won_1 = require("../leads/won");
23
+ const tenancy_1 = require("../tenancy");
23
24
  const paths_1 = require("./paths");
24
25
  __exportStar(require("./dto"), exports);
25
26
  __exportStar(require("./paths"), exports);
26
- /** Storage bucket every supporting file lives in. */
27
- const BUCKET = 'supporting-files';
28
27
  /** How long a presigned download URL stays valid. */
29
28
  const SIGNED_URL_TTL_SECONDS = 300;
30
29
  /** Storage calls per file (minting an upload URL, checking an object) run concurrently in batches of this size. */
@@ -60,41 +59,40 @@ class SupportingFilesResource {
60
59
  entityType: enum_2.AuditTrailEntityType.SUPPORTING_FILES,
61
60
  });
62
61
  }
62
+ /**
63
+ * A short-lived download link. Storage checks the bucket's read policy as this client's
64
+ * caller, which names no staff - the admin api signs on the service client's organization
65
+ * scope instead.
66
+ */
63
67
  async getPresignedUrl(supportingFileId) {
64
- const { data, error } = await this.supabase
65
- .from('supporting_files')
66
- .select('name, extension, entity_type, entity_id')
68
+ const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('supporting_files').select('organization_id, name, extension, entity_type, entity_id'), this.auth)
67
69
  .eq('mms_id', supportingFileId)
68
- .eq('organization_id', this.auth.organization_id)
69
70
  .single();
70
71
  if (error)
71
72
  throw (0, errors_1.mapPostgrestError)(error);
72
73
  const { data: signed, error: signedError } = await this.supabase.storage
73
- .from(BUCKET)
74
- .createSignedUrl((0, paths_1.storagePath)(this.auth.organization_id, data.entity_type, data.entity_id, data.name, data.extension), SIGNED_URL_TTL_SECONDS);
74
+ .from(paths_1.SUPPORTING_FILES_BUCKET)
75
+ .createSignedUrl((0, paths_1.storagePath)(data.organization_id, data.entity_type, data.entity_id, data.name, data.extension), SIGNED_URL_TTL_SECONDS);
75
76
  if (signedError)
76
77
  throw (0, errors_1.mapStorageError)(signedError);
77
78
  return { url: signed.signedUrl, ttl: SIGNED_URL_TTL_SECONDS };
78
79
  }
79
- /** The record a file hangs off - what the api checks the caller can reach before a download or delete. */
80
+ /**
81
+ * The record a file hangs off, and its organization - what the api checks the caller can
82
+ * reach before a download or delete.
83
+ */
80
84
  async getParent(supportingFileId) {
81
- const { data, error } = await this.supabase
82
- .from('supporting_files')
83
- .select('entity_type, entity_id')
85
+ const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('supporting_files').select('entity_type, entity_id, organization_id'), this.auth)
84
86
  .eq('mms_id', supportingFileId)
85
- .eq('organization_id', this.auth.organization_id)
86
87
  .single();
87
88
  if (error)
88
89
  throw (0, errors_1.mapPostgrestError)(error);
89
90
  return data;
90
91
  }
91
92
  async list(entity, entityId) {
92
- const { data, error } = await this.supabase
93
- .from('supporting_files')
94
- .select('mms_id, name, extension')
93
+ const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('supporting_files').select('mms_id, name, extension'), this.auth)
95
94
  .eq('entity_type', entity)
96
95
  .eq('entity_id', entityId)
97
- .eq('organization_id', this.auth.organization_id)
98
96
  .order('created_at', { ascending: false });
99
97
  if (error)
100
98
  throw (0, errors_1.mapPostgrestError)(error);
@@ -112,7 +110,7 @@ class SupportingFilesResource {
112
110
  for (let index = 0; index < files.length; index += BATCH_SIZE) {
113
111
  targets.push(...(await Promise.all(files.slice(index, index + BATCH_SIZE).map(async ({ name, extension, path }) => {
114
112
  const { data, error } = await this.supabase.storage
115
- .from(BUCKET)
113
+ .from(paths_1.SUPPORTING_FILES_BUCKET)
116
114
  .createSignedUploadUrl(path, { upsert: true });
117
115
  if (error) {
118
116
  throw (0, errors_1.mapStorageError)(error, `Failed to prepare upload: ${fileNameOf({ name, extension })}`);
@@ -128,12 +126,13 @@ class SupportingFilesResource {
128
126
  * object was refreshed in place). Returns the record's whole list.
129
127
  */
130
128
  async record(entity, entityId, names) {
129
+ const organizationId = (0, tenancy_1.requireOrganization)(this.auth);
131
130
  const files = await this.prepare(entity, entityId, names);
132
131
  for (let index = 0; index < files.length; index += BATCH_SIZE) {
133
132
  await Promise.all(files.slice(index, index + BATCH_SIZE).map(async ({ name, extension, path }) => {
134
133
  // A missing object comes back as `data: false` (storage-js keeps the 404 in `error`);
135
134
  // any other failure is thrown by storage-js itself.
136
- const { data: present } = await this.supabase.storage.from(BUCKET).exists(path);
135
+ const { data: present } = await this.supabase.storage.from(paths_1.SUPPORTING_FILES_BUCKET).exists(path);
137
136
  if (!present) {
138
137
  throw new errors_1.SupabaseClientError({
139
138
  status: 400,
@@ -148,7 +147,7 @@ class SupportingFilesResource {
148
147
  .select('name, extension')
149
148
  .eq('entity_type', entity)
150
149
  .eq('entity_id', entityId)
151
- .eq('organization_id', this.auth.organization_id);
150
+ .eq('organization_id', organizationId);
152
151
  if (existingError)
153
152
  throw (0, errors_1.mapPostgrestError)(existingError);
154
153
  const existingFileNames = existing.map(fileNameOf);
@@ -159,7 +158,7 @@ class SupportingFilesResource {
159
158
  extension,
160
159
  entity_type: entity,
161
160
  entity_id: entityId,
162
- organization_id: this.auth.organization_id,
161
+ organization_id: organizationId,
163
162
  created_at: new Date().toISOString(),
164
163
  created_by: this.auth.mms_id,
165
164
  })));
@@ -170,9 +169,9 @@ class SupportingFilesResource {
170
169
  // The row goes first - the helper's pre-read hands back what the object path is built from.
171
170
  const data = await this.table.remove(supportingFileId);
172
171
  const { error: storageError } = await this.supabase.storage
173
- .from(BUCKET)
172
+ .from(paths_1.SUPPORTING_FILES_BUCKET)
174
173
  .remove([
175
- (0, paths_1.storagePath)(this.auth.organization_id, data.entity_type, data.entity_id, data.name, data.extension),
174
+ (0, paths_1.storagePath)((0, tenancy_1.requireOrganization)(this.auth), data.entity_type, data.entity_id, data.name, data.extension),
176
175
  ]);
177
176
  if (storageError) {
178
177
  const fileName = fileNameOf({
@@ -188,8 +187,9 @@ class SupportingFilesResource {
188
187
  * are one key. A converted lead takes no new files.
189
188
  */
190
189
  async prepare(entity, entityId, names) {
190
+ const organizationId = (0, tenancy_1.requireOrganization)(this.auth);
191
191
  if (entity === enum_1.SupportingFilesEntityTypes.LEAD) {
192
- await (0, won_1.refuseWonLead)(this.supabase, this.auth.organization_id, entityId);
192
+ await (0, won_1.refuseWonLead)(this.supabase, organizationId, entityId);
193
193
  }
194
194
  return names.map((fileName) => {
195
195
  const { name, extension } = (0, paths_1.splitFileName)((0, paths_1.sanitizeFileName)(fileName));
@@ -203,7 +203,7 @@ class SupportingFilesResource {
203
203
  return {
204
204
  name,
205
205
  extension: extension,
206
- path: (0, paths_1.storagePath)(this.auth.organization_id, entity, entityId, name, extension),
206
+ path: (0, paths_1.storagePath)(organizationId, entity, entityId, name, extension),
207
207
  };
208
208
  });
209
209
  }
@@ -1,3 +1,5 @@
1
+ /** Storage bucket every supporting file lives in, and every document image. */
2
+ export declare const SUPPORTING_FILES_BUCKET = "supporting-files";
1
3
  /**
2
4
  * The client-supplied file name, checked before it is allowed anywhere near an object key.
3
5
  *