@managemint-solutions/sdk 0.56.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 (53) 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/notifications/index.d.ts +6 -0
  31. package/dist/notifications/index.js +16 -0
  32. package/dist/organization-payment-methods/dto.d.ts +10 -0
  33. package/dist/organization-payment-methods/dto.js +56 -0
  34. package/dist/organization-payment-methods/index.d.ts +8 -1
  35. package/dist/organization-payment-methods/index.js +23 -0
  36. package/dist/payments/index.d.ts +67 -0
  37. package/dist/payments/index.js +137 -0
  38. package/dist/permissions/dto.d.ts +4 -0
  39. package/dist/permissions/dto.js +20 -8
  40. package/dist/service-client/index.d.ts +32 -0
  41. package/dist/service-client/index.js +60 -0
  42. package/dist/staff-client.d.ts +3 -0
  43. package/dist/staff-client.js +5 -0
  44. package/dist/supporting-files/index.js +4 -6
  45. package/dist/supporting-files/paths.d.ts +2 -0
  46. package/dist/supporting-files/paths.js +3 -1
  47. package/dist/validators.d.ts +6 -0
  48. package/dist/validators.js +14 -1
  49. package/dist/visibility/dto.d.ts +15 -0
  50. package/dist/visibility/dto.js +68 -0
  51. package/dist/visibility/index.d.ts +59 -0
  52. package/dist/visibility/index.js +165 -0
  53. 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,11 +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';
16
18
  import { SupportingFilesResource } from '../supporting-files';
17
19
  import { UserApiCredentialsResource } from '../user-api-credentials';
18
20
  import { UsersResource } from '../users';
21
+ import { VisibilityResource } from '../visibility';
19
22
  import { WebhookEventsResource } from '../webhook-events';
20
23
  export type SupabaseServiceClientConfig = {
21
24
  url: string;
@@ -34,6 +37,13 @@ export declare class SupabaseOrganizationScope {
34
37
  readonly actor: AuditActor;
35
38
  readonly audit: AuditResource;
36
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;
37
47
  readonly invoices: InvoicesResource;
38
48
  readonly invites: OrganizationInvitesResource;
39
49
  readonly leaveTypes: LeaveTypesResource;
@@ -45,6 +55,8 @@ export declare class SupabaseOrganizationScope {
45
55
  /** For the admin api's staff downloads: Storage's read policy names no staff, the service role passes it. */
46
56
  readonly supportingFiles: SupportingFilesResource;
47
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;
48
60
  constructor(supabase: TypedSupabaseClient, actor: AuditActor, onAuditError?: AuditErrorHandler);
49
61
  /**
50
62
  * An audited table bound to this organization and actor - the shape the api hand-rolls today
@@ -153,6 +165,26 @@ export declare class SupabaseServiceClient {
153
165
  user_id: string;
154
166
  organization_id: string;
155
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>;
156
188
  /**
157
189
  * Deletes every audit row written before `cutoff` and says how many went - the daily
158
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,11 +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");
21
24
  const supporting_files_1 = require("../supporting-files");
22
25
  const user_api_credentials_1 = require("../user-api-credentials");
23
26
  const users_1 = require("../users");
27
+ const visibility_1 = require("../visibility");
24
28
  const webhook_events_1 = require("../webhook-events");
25
29
  /**
26
30
  * The organization-scoped half of the service client. Everything here is attributed to one
@@ -32,6 +36,13 @@ class SupabaseOrganizationScope {
32
36
  actor;
33
37
  audit;
34
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;
35
46
  invoices;
36
47
  invites;
37
48
  leaveTypes;
@@ -43,11 +54,15 @@ class SupabaseOrganizationScope {
43
54
  /** For the admin api's staff downloads: Storage's read policy names no staff, the service role passes it. */
44
55
  supportingFiles;
45
56
  users;
57
+ /** Who private records are shared with: listed and written here, after the api's own checks. */
58
+ visibility;
46
59
  constructor(supabase, actor, onAuditError) {
47
60
  this.supabase = supabase;
48
61
  this.actor = actor;
49
62
  this.audit = new audit_1.AuditResource(supabase, actor, onAuditError);
50
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);
51
66
  this.invoices = new invoices_1.InvoicesResource(supabase, actor, this.audit);
52
67
  this.invites = new organization_invites_1.OrganizationInvitesResource(supabase, actor);
53
68
  // The leave-year rollover writes next period's entitlements as the system (`mms_id: null`).
@@ -59,6 +74,7 @@ class SupabaseOrganizationScope {
59
74
  this.permissions = new permissions_1.PermissionsResource(supabase, actor, this.audit);
60
75
  this.supportingFiles = new supporting_files_1.SupportingFilesResource(supabase, actor, this.audit);
61
76
  this.users = new users_1.UsersResource(supabase, actor, this.audit);
77
+ this.visibility = new visibility_1.VisibilityResource(supabase, actor, this.audit);
62
78
  }
63
79
  /**
64
80
  * An audited table bound to this organization and actor - the shape the api hand-rolls today
@@ -266,6 +282,50 @@ class SupabaseServiceClient {
266
282
  throw (0, errors_1.mapPostgrestError)(error);
267
283
  return (data ?? null);
268
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
+ }
269
329
  /**
270
330
  * Deletes every audit row written before `cutoff` and says how many went - the daily
271
331
  * retention cron. Every tenant at once: the period is the same for all of them. Batched by
@@ -3,6 +3,7 @@ import { AuditResource } from './audit';
3
3
  import { AdditionalDetailsResource } from './additional-details';
4
4
  import type { SupabaseClientConfig, TypedSupabaseClient } from './client';
5
5
  import { ClientsResource } from './clients';
6
+ import { DocumentsResource, DocumentTemplatesResource } from './documents';
6
7
  import { LeadsResource } from './leads';
7
8
  import { NotesResource } from './notes';
8
9
  import { OrganizationsResource } from './organizations';
@@ -31,6 +32,8 @@ export declare class SupabaseStaffClient {
31
32
  readonly notes: NotesResource;
32
33
  readonly projects: ProjectsResource;
33
34
  readonly tasks: TasksResource;
35
+ readonly documentTemplates: DocumentTemplatesResource;
36
+ readonly documents: DocumentsResource;
34
37
  readonly users: UsersResource;
35
38
  readonly organizations: OrganizationsResource;
36
39
  readonly staff: StaffResource;
@@ -5,6 +5,7 @@ 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");
9
10
  const notes_1 = require("./notes");
10
11
  const organizations_1 = require("./organizations");
@@ -30,6 +31,8 @@ class SupabaseStaffClient {
30
31
  notes;
31
32
  projects;
32
33
  tasks;
34
+ documentTemplates;
35
+ documents;
33
36
  users;
34
37
  organizations;
35
38
  staff;
@@ -47,6 +50,8 @@ class SupabaseStaffClient {
47
50
  this.notes = new notes_1.NotesResource(this.supabase, auth, this.audit);
48
51
  this.projects = new projects_1.ProjectsResource(this.supabase, auth, this.audit);
49
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);
50
55
  this.users = new users_1.UsersResource(this.supabase, auth, this.audit);
51
56
  this.organizations = new organizations_1.OrganizationsResource(this.supabase, auth, this.audit);
52
57
  this.staff = new staff_1.StaffResource(this.supabase);
@@ -24,8 +24,6 @@ const tenancy_1 = require("../tenancy");
24
24
  const paths_1 = require("./paths");
25
25
  __exportStar(require("./dto"), exports);
26
26
  __exportStar(require("./paths"), exports);
27
- /** Storage bucket every supporting file lives in. */
28
- const BUCKET = 'supporting-files';
29
27
  /** How long a presigned download URL stays valid. */
30
28
  const SIGNED_URL_TTL_SECONDS = 300;
31
29
  /** Storage calls per file (minting an upload URL, checking an object) run concurrently in batches of this size. */
@@ -73,7 +71,7 @@ class SupportingFilesResource {
73
71
  if (error)
74
72
  throw (0, errors_1.mapPostgrestError)(error);
75
73
  const { data: signed, error: signedError } = await this.supabase.storage
76
- .from(BUCKET)
74
+ .from(paths_1.SUPPORTING_FILES_BUCKET)
77
75
  .createSignedUrl((0, paths_1.storagePath)(data.organization_id, data.entity_type, data.entity_id, data.name, data.extension), SIGNED_URL_TTL_SECONDS);
78
76
  if (signedError)
79
77
  throw (0, errors_1.mapStorageError)(signedError);
@@ -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 })}`);
@@ -134,7 +132,7 @@ class SupportingFilesResource {
134
132
  await Promise.all(files.slice(index, index + BATCH_SIZE).map(async ({ name, extension, path }) => {
135
133
  // A missing object comes back as `data: false` (storage-js keeps the 404 in `error`);
136
134
  // any other failure is thrown by storage-js itself.
137
- 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);
138
136
  if (!present) {
139
137
  throw new errors_1.SupabaseClientError({
140
138
  status: 400,
@@ -171,7 +169,7 @@ class SupportingFilesResource {
171
169
  // The row goes first - the helper's pre-read hands back what the object path is built from.
172
170
  const data = await this.table.remove(supportingFileId);
173
171
  const { error: storageError } = await this.supabase.storage
174
- .from(BUCKET)
172
+ .from(paths_1.SUPPORTING_FILES_BUCKET)
175
173
  .remove([
176
174
  (0, paths_1.storagePath)((0, tenancy_1.requireOrganization)(this.auth), data.entity_type, data.entity_id, data.name, data.extension),
177
175
  ]);
@@ -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
  *
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.storagePath = exports.splitFileName = exports.sanitizeFileName = void 0;
3
+ exports.storagePath = exports.splitFileName = exports.sanitizeFileName = exports.SUPPORTING_FILES_BUCKET = void 0;
4
4
  const errors_1 = require("../errors");
5
+ /** Storage bucket every supporting file lives in, and every document image. */
6
+ exports.SUPPORTING_FILES_BUCKET = 'supporting-files';
5
7
  /** Longest object-key segment a file name may become. */
6
8
  const MAX_FILE_NAME_LENGTH = 255;
7
9
  /** What a file name may be made of - no separators, no control characters, no leading dot. */
@@ -26,3 +26,9 @@ export declare function IsNullable(validationOptions?: ValidationOptions): Prope
26
26
  * blank submission tripping it. Goes above `IsNullable()`, which then skips the rules.
27
27
  */
28
28
  export declare function EmptyToNull(): PropertyDecorator;
29
+ /**
30
+ * A class-validator decorator from a check that returns the first problem it finds, or null. The
31
+ * problem is the message, so a 400 always says exactly what is wrong and where. `object` is the
32
+ * DTO being validated, for a check that reads a sibling field.
33
+ */
34
+ export declare const fromProblem: (name: string, problem: (value: unknown, object: Record<string, unknown>) => string | null) => () => PropertyDecorator;
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.MatchesPasswordRules = exports.IsClockTime = exports.isClockTime = exports.IsCalendarDate = exports.isCalendarDate = void 0;
9
+ exports.fromProblem = exports.MatchesPasswordRules = exports.IsClockTime = exports.isClockTime = exports.IsCalendarDate = exports.isCalendarDate = void 0;
10
10
  exports.IsNullable = IsNullable;
11
11
  exports.EmptyToNull = EmptyToNull;
12
12
  const class_validator_1 = require("class-validator");
@@ -72,3 +72,16 @@ function IsNullable(validationOptions) {
72
72
  function EmptyToNull() {
73
73
  return (0, class_transformer_1.Transform)(({ value }) => (value === '' ? null : value), { toClassOnly: true });
74
74
  }
75
+ /**
76
+ * A class-validator decorator from a check that returns the first problem it finds, or null. The
77
+ * problem is the message, so a 400 always says exactly what is wrong and where. `object` is the
78
+ * DTO being validated, for a check that reads a sibling field.
79
+ */
80
+ const fromProblem = (name, problem) => () => (0, class_validator_1.ValidateBy)({
81
+ name,
82
+ validator: {
83
+ validate: (value, args) => problem(value, args?.object) === null,
84
+ defaultMessage: (args) => problem(args?.value, args?.object) ?? 'Invalid value',
85
+ },
86
+ });
87
+ exports.fromProblem = fromProblem;
@@ -0,0 +1,15 @@
1
+ import { DocumentVisibility } from '@managemint-solutions/entities/documents/enum';
2
+ import type { UpdateVisibilityDto as UpdateVisibilityDtoType } from '@managemint-solutions/entities/visibility/dto';
3
+ import { VisibilityRole } from '@managemint-solutions/entities/visibility/enum';
4
+ /**
5
+ * A record's setting and its whole share list, replaced together. That the people belong to the
6
+ * organization and the creator is not among them is the api's check (the composite key refuses
7
+ * another organization's user too).
8
+ */
9
+ export declare class UpdateVisibilityDto implements UpdateVisibilityDtoType {
10
+ readonly visibility: DocumentVisibility;
11
+ readonly shares: {
12
+ user_id: string;
13
+ role: VisibilityRole;
14
+ }[];
15
+ }
@@ -0,0 +1,68 @@
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.UpdateVisibilityDto = void 0;
13
+ const class_validator_1 = require("class-validator");
14
+ const enum_1 = require("@managemint-solutions/entities/documents/enum");
15
+ const enum_2 = require("@managemint-solutions/entities/visibility/enum");
16
+ const validators_1 = require("../validators");
17
+ // class-validator version of the shared UpdateVisibilityDto from @managemint-solutions/entities.
18
+ /** More people than any record is shared with. */
19
+ const MAX_SHARES = 100;
20
+ const ROLES = new Set(Object.values(enum_2.VisibilityRole));
21
+ /**
22
+ * The share list's first problem, named by its position, or null: each `{ user_id, role }`
23
+ * exactly, a v4 user id (a `users.mms_id`, the GoTrue auth uid), a known role, and each person
24
+ * once. One check rather than nested DTOs, so the api answers with this message instead of a
25
+ * nested error's empty one.
26
+ */
27
+ const IsVisibilityShares = (0, validators_1.fromProblem)('isVisibilityShares', (shares) => {
28
+ if (!Array.isArray(shares))
29
+ return 'Shares must be a list';
30
+ if (shares.length > MAX_SHARES)
31
+ return `Cannot share with more than ${MAX_SHARES} people`;
32
+ const people = new Set();
33
+ for (const [index, share] of shares.entries()) {
34
+ const at = `Share ${index + 1}`;
35
+ if (typeof share !== 'object' || share === null || Array.isArray(share))
36
+ return `${at} must be an object`;
37
+ const extra = Object.keys(share).find((key) => key !== 'user_id' && key !== 'role');
38
+ if (extra)
39
+ return `${at}: Unexpected property "${extra}"`;
40
+ const { user_id, role } = share;
41
+ if (typeof user_id !== 'string' || !(0, class_validator_1.isUUID)(user_id, '4'))
42
+ return `${at}: Invalid user ID format`;
43
+ if (typeof role !== 'string' || !ROLES.has(role))
44
+ return `${at}: Role must be one of ${[...ROLES].join(', ')}`;
45
+ if (people.has(user_id))
46
+ return `${at}: This person is already in the list`;
47
+ people.add(user_id);
48
+ }
49
+ return null;
50
+ });
51
+ /**
52
+ * A record's setting and its whole share list, replaced together. That the people belong to the
53
+ * organization and the creator is not among them is the api's check (the composite key refuses
54
+ * another organization's user too).
55
+ */
56
+ class UpdateVisibilityDto {
57
+ visibility;
58
+ shares;
59
+ }
60
+ exports.UpdateVisibilityDto = UpdateVisibilityDto;
61
+ __decorate([
62
+ (0, class_validator_1.IsEnum)(enum_1.DocumentVisibility, { message: `Visibility must be one of: ${Object.values(enum_1.DocumentVisibility).join(', ')}` }),
63
+ __metadata("design:type", String)
64
+ ], UpdateVisibilityDto.prototype, "visibility", void 0);
65
+ __decorate([
66
+ IsVisibilityShares(),
67
+ __metadata("design:type", Array)
68
+ ], UpdateVisibilityDto.prototype, "shares", void 0);