@managemint-solutions/sdk 0.49.0 → 0.51.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.d.ts CHANGED
@@ -17,7 +17,7 @@ export type { CreateFeatureRequestRow, CreatedFeatureRequest } from './feature-r
17
17
  export { CreateFeatureRequestDto } from './feature-requests';
18
18
  export { CreateConfigDto, UpdateConfigDto } from './configs';
19
19
  export { CreateSupportRequestDto } from './support';
20
- export { AddStatusDto, GetStatusesDto, StatusIdDto, UpdateStatusDto } from './statuses';
20
+ export { AddStatusDto, GetStatusesDto, ReorderStatusesDto, StatusIdDto, UpdateStatusDto } from './statuses';
21
21
  export { AddLabelDto, GetLabelsDto, LabelIdDto, UpdateLabelDto } from './labels';
22
22
  export { AddAdditionalDetailsDto, AdditionalDetailsIdDto, GetAdditionalDetailsDto, IsAdditionalDetailsValueByType, UpdateAdditionalDetailsDto, } from './additional-details';
23
23
  export { AddNoteDto, GetNotesDto, NoteIdDto } from './notes';
@@ -33,7 +33,7 @@ export { UpdatePermissionsDto } from './permissions';
33
33
  export type { UserEmploymentRow, UserRow } from './users';
34
34
  export { AddUserDto, GetUsersQueryDto, UpdateMyProfileDto, UpdateUserDto, UserIdDto, } from './users';
35
35
  export type { ClaimOutcome, WebhookEventRow, WebhookEventsTable } from './webhook-events';
36
- export { GetSupportingFilesDto, SupportingFileIdDto } from './supporting-files';
36
+ export { GetSupportingFilesDto, SupportingFileIdDto, SupportingFileNamesDto } from './supporting-files';
37
37
  export { ClientIdDto, CreateClientDto, GetClientsDto, UpdateClientDto } from './clients';
38
38
  export { ConvertLeadDto, CreateLeadDto, GetLeadsDto, LeadIdDto, UpdateLeadDto } from './leads';
39
39
  export { CreateProjectDto, GetProjectsDto, ProjectIdDto, UpdateProjectDto } from './projects';
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.AddUserDto = 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.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.shiftDays = exports.proRataDays = 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.SupportingFileIdDto = exports.GetSupportingFilesDto = exports.UserIdDto = exports.UpdateUserDto = exports.UpdateMyProfileDto = exports.GetUsersQueryDto = 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 = void 0;
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;
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; } });
@@ -52,6 +52,7 @@ Object.defineProperty(exports, "CreateSupportRequestDto", { enumerable: true, ge
52
52
  var statuses_1 = require("./statuses");
53
53
  Object.defineProperty(exports, "AddStatusDto", { enumerable: true, get: function () { return statuses_1.AddStatusDto; } });
54
54
  Object.defineProperty(exports, "GetStatusesDto", { enumerable: true, get: function () { return statuses_1.GetStatusesDto; } });
55
+ Object.defineProperty(exports, "ReorderStatusesDto", { enumerable: true, get: function () { return statuses_1.ReorderStatusesDto; } });
55
56
  Object.defineProperty(exports, "StatusIdDto", { enumerable: true, get: function () { return statuses_1.StatusIdDto; } });
56
57
  Object.defineProperty(exports, "UpdateStatusDto", { enumerable: true, get: function () { return statuses_1.UpdateStatusDto; } });
57
58
  var labels_1 = require("./labels");
@@ -90,6 +91,7 @@ Object.defineProperty(exports, "UserIdDto", { enumerable: true, get: function ()
90
91
  var supporting_files_1 = require("./supporting-files");
91
92
  Object.defineProperty(exports, "GetSupportingFilesDto", { enumerable: true, get: function () { return supporting_files_1.GetSupportingFilesDto; } });
92
93
  Object.defineProperty(exports, "SupportingFileIdDto", { enumerable: true, get: function () { return supporting_files_1.SupportingFileIdDto; } });
94
+ Object.defineProperty(exports, "SupportingFileNamesDto", { enumerable: true, get: function () { return supporting_files_1.SupportingFileNamesDto; } });
93
95
  var clients_1 = require("./clients");
94
96
  Object.defineProperty(exports, "ClientIdDto", { enumerable: true, get: function () { return clients_1.ClientIdDto; } });
95
97
  Object.defineProperty(exports, "CreateClientDto", { enumerable: true, get: function () { return clients_1.CreateClientDto; } });
@@ -1,4 +1,4 @@
1
- import type { AddStatusDto as AddStatusDtoType, GetStatusesDto as GetStatusesDtoType, StatusIdDto as StatusIdDtoType, UpdateStatusDto as UpdateStatusDtoType } from '@managemint-solutions/entities/status/dto';
1
+ import type { AddStatusDto as AddStatusDtoType, GetStatusesDto as GetStatusesDtoType, ReorderStatusesDto as ReorderStatusesDtoType, StatusIdDto as StatusIdDtoType, UpdateStatusDto as UpdateStatusDtoType } from '@managemint-solutions/entities/status/dto';
2
2
  import { StatusEntityTypes } from '@managemint-solutions/entities/status/enum';
3
3
  export declare class StatusIdDto implements StatusIdDtoType {
4
4
  readonly statusId: string;
@@ -12,6 +12,9 @@ export declare class AddStatusDto implements AddStatusDtoType {
12
12
  readonly entity: StatusEntityTypes;
13
13
  readonly is_complete?: boolean;
14
14
  }
15
+ export declare class ReorderStatusesDto implements ReorderStatusesDtoType {
16
+ readonly status_ids: string[];
17
+ }
15
18
  export declare class UpdateStatusDto implements UpdateStatusDtoType {
16
19
  readonly label: string;
17
20
  readonly colour: string;
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.UpdateStatusDto = exports.AddStatusDto = exports.GetStatusesDto = exports.StatusIdDto = void 0;
12
+ exports.UpdateStatusDto = exports.ReorderStatusesDto = exports.AddStatusDto = exports.GetStatusesDto = exports.StatusIdDto = void 0;
13
13
  const class_validator_1 = require("class-validator");
14
14
  const enum_1 = require("@managemint-solutions/entities/status/enum");
15
15
  // class-validator versions of the shared DTO types from @managemint-solutions/entities.
@@ -62,6 +62,18 @@ __decorate([
62
62
  (0, class_validator_1.IsBoolean)({ message: 'is_complete must be true or false' }),
63
63
  __metadata("design:type", Boolean)
64
64
  ], AddStatusDto.prototype, "is_complete", void 0);
65
+ class ReorderStatusesDto {
66
+ // The whole catalogue of the entity type in its new order; the resource checks it is complete
67
+ // and has no repeats, which the decorators cannot (they do not know the organization).
68
+ status_ids;
69
+ }
70
+ exports.ReorderStatusesDto = ReorderStatusesDto;
71
+ __decorate([
72
+ (0, class_validator_1.IsArray)({ message: 'status_ids must be an array' }),
73
+ (0, class_validator_1.ArrayMinSize)(1, { message: 'status_ids cannot be empty' }),
74
+ (0, class_validator_1.IsUUID)('7', { each: true, message: 'Invalid status ID format' }),
75
+ __metadata("design:type", Array)
76
+ ], ReorderStatusesDto.prototype, "status_ids", void 0);
65
77
  class UpdateStatusDto {
66
78
  label;
67
79
  // Colour must be a valid hex code (e.g., #FFFFFF)
@@ -1,5 +1,5 @@
1
1
  import type { StatusEntity } from '@managemint-solutions/entities/status';
2
- import type { AddStatusDto, UpdateStatusDto } from '@managemint-solutions/entities/status/dto';
2
+ import type { AddStatusDto, ReorderStatusesDto, UpdateStatusDto } from '@managemint-solutions/entities/status/dto';
3
3
  import type { StatusEntityTypes } from '@managemint-solutions/entities/status/enum';
4
4
  import type { AuthContext } from '../auth';
5
5
  import { type AuditResource } from '../audit';
@@ -10,8 +10,17 @@ export declare class StatusesResource {
10
10
  private readonly auth;
11
11
  private readonly table;
12
12
  constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
13
+ /** The catalogue in its order: `position` first, then insertion order for the seeded ties. */
13
14
  list(entity: StatusEntityTypes): Promise<StatusEntity[]>;
15
+ /** A new status goes last: one past the highest position its entity type has. */
14
16
  create(input: AddStatusDto): Promise<StatusEntity>;
17
+ /**
18
+ * Rewrites the whole catalogue's positions from the order given. The ids must be exactly the
19
+ * entity type's statuses, each once - a partial list would leave positions undefined, so it is
20
+ * refused with a 400 before anything is written. Only the rows that move are written (and
21
+ * audited); the answer is the catalogue in its new order.
22
+ */
23
+ reorder(entity: StatusEntityTypes, input: ReorderStatusesDto): Promise<StatusEntity[]>;
15
24
  update(statusId: string, input: UpdateStatusDto): Promise<StatusEntity>;
16
25
  /** The Won status cannot be deleted. Every other status can. */
17
26
  remove(statusId: string): Promise<void>;
@@ -23,7 +23,7 @@ const refuseWon = (message) => new errors_1.SupabaseClientError({ status: 400, e
23
23
  // The public contract is the shared StatusEntity from @managemint-solutions/entities; the
24
24
  // `as StatusEntity` casts declare the shape the select string below produces (the columns are
25
25
  // defined in the api's supabase/migrations).
26
- const STATUS_SELECT = 'mms_id, label, colour, is_complete, is_fail, is_triage, is_won';
26
+ const STATUS_SELECT = 'mms_id, label, colour, is_complete, is_fail, is_triage, is_won, position';
27
27
  // The audited write returns the whole row; the wire shape is the STATUS_SELECT columns.
28
28
  const toStatus = (row) => ({
29
29
  mms_id: row.mms_id,
@@ -33,6 +33,7 @@ const toStatus = (row) => ({
33
33
  is_fail: row.is_fail,
34
34
  is_triage: row.is_triage,
35
35
  is_won: row.is_won,
36
+ position: row.position,
36
37
  });
37
38
  class StatusesResource {
38
39
  supabase;
@@ -46,27 +47,75 @@ class StatusesResource {
46
47
  entityType: enum_1.AuditTrailEntityType.STATUS,
47
48
  });
48
49
  }
50
+ /** The catalogue in its order: `position` first, then insertion order for the seeded ties. */
49
51
  async list(entity) {
50
52
  const { data, error } = await this.supabase
51
53
  .from('statuses')
52
54
  .select(STATUS_SELECT)
53
55
  .eq('organization_id', this.auth.organization_id)
54
- .eq('entity_type', entity);
56
+ .eq('entity_type', entity)
57
+ .order('position')
58
+ .order('created_at')
59
+ .order('mms_id');
55
60
  if (error)
56
61
  throw (0, errors_1.mapPostgrestError)(error);
57
62
  return data;
58
63
  }
64
+ /** A new status goes last: one past the highest position its entity type has. */
59
65
  async create(input) {
66
+ const { data, error } = await this.supabase
67
+ .from('statuses')
68
+ .select('position')
69
+ .eq('organization_id', this.auth.organization_id)
70
+ .eq('entity_type', input.entity)
71
+ .order('position', { ascending: false })
72
+ .limit(1)
73
+ .maybeSingle();
74
+ if (error)
75
+ throw (0, errors_1.mapPostgrestError)(error);
76
+ const last = data?.position;
60
77
  const row = await this.table.insert({
61
78
  label: input.label,
62
79
  colour: input.colour,
63
80
  entity_type: input.entity,
64
81
  is_complete: input.is_complete ?? false,
82
+ position: last === undefined ? 0 : last + 1,
65
83
  organization_id: this.auth.organization_id,
66
84
  created_by: this.auth.mms_id,
67
85
  });
68
86
  return toStatus(row);
69
87
  }
88
+ /**
89
+ * Rewrites the whole catalogue's positions from the order given. The ids must be exactly the
90
+ * entity type's statuses, each once - a partial list would leave positions undefined, so it is
91
+ * refused with a 400 before anything is written. Only the rows that move are written (and
92
+ * audited); the answer is the catalogue in its new order.
93
+ */
94
+ async reorder(entity, input) {
95
+ const current = await this.list(entity);
96
+ const currentIds = new Set(current.map((status) => status.mms_id));
97
+ const given = new Set(input.status_ids);
98
+ if (given.size !== input.status_ids.length ||
99
+ given.size !== currentIds.size ||
100
+ input.status_ids.some((id) => !currentIds.has(id))) {
101
+ throw new errors_1.SupabaseClientError({
102
+ status: 400,
103
+ error: 'Database Error',
104
+ message: `status_ids must list every ${entity} status exactly once`,
105
+ });
106
+ }
107
+ const positionOf = new Map(current.map((status) => [status.mms_id, status.position]));
108
+ for (const [position, id] of input.status_ids.entries()) {
109
+ if (positionOf.get(id) === position)
110
+ continue;
111
+ await this.table.update(id, {
112
+ position,
113
+ updated_at: new Date().toISOString(),
114
+ last_updated_by: this.auth.mms_id,
115
+ });
116
+ }
117
+ return this.list(entity);
118
+ }
70
119
  async update(statusId, input) {
71
120
  if (await this.isWon(statusId)) {
72
121
  throw refuseWon('The Won status is fixed and cannot be changed');
@@ -1,5 +1,7 @@
1
- import type { GetSupportingFilesDto as GetSupportingFilesDtoType, SupportingFileIdDto as SupportingFileIdDtoType } from '@managemint-solutions/entities/supporting-files/dto';
1
+ import type { GetSupportingFilesDto as GetSupportingFilesDtoType, SupportingFileIdDto as SupportingFileIdDtoType, SupportingFileNamesDto as SupportingFileNamesDtoType } from '@managemint-solutions/entities/supporting-files/dto';
2
2
  import { SupportingFilesEntityTypes } from '@managemint-solutions/entities/supporting-files/enum';
3
+ /** Files accepted in one upload. */
4
+ export declare const MAX_FILES_PER_UPLOAD = 10;
3
5
  export declare class SupportingFileIdDto implements SupportingFileIdDtoType {
4
6
  readonly supportingFileId: string;
5
7
  }
@@ -7,3 +9,6 @@ export declare class GetSupportingFilesDto implements GetSupportingFilesDtoType
7
9
  readonly entity: SupportingFilesEntityTypes;
8
10
  readonly entityId: string;
9
11
  }
12
+ export declare class SupportingFileNamesDto implements SupportingFileNamesDtoType {
13
+ readonly names: string[];
14
+ }
@@ -9,12 +9,13 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.GetSupportingFilesDto = exports.SupportingFileIdDto = void 0;
12
+ exports.SupportingFileNamesDto = exports.GetSupportingFilesDto = exports.SupportingFileIdDto = exports.MAX_FILES_PER_UPLOAD = void 0;
13
13
  const class_validator_1 = require("class-validator");
14
14
  const enum_1 = require("@managemint-solutions/entities/supporting-files/enum");
15
15
  // class-validator versions of the shared DTO types from @managemint-solutions/entities.
16
- // Each class `implements` its entities type so the wire contract cannot drift. There is no class
17
- // for the upload payload - it arrives as multipart and is not validated by class-validator.
16
+ // Each class `implements` its entities type so the wire contract cannot drift.
17
+ /** Files accepted in one upload. */
18
+ exports.MAX_FILES_PER_UPLOAD = 10;
18
19
  class SupportingFileIdDto {
19
20
  supportingFileId;
20
21
  }
@@ -36,3 +37,17 @@ __decorate([
36
37
  (0, class_validator_1.IsUUID)('7', { message: 'Invalid entity ID format' }),
37
38
  __metadata("design:type", String)
38
39
  ], GetSupportingFilesDto.prototype, "entityId", void 0);
40
+ class SupportingFileNamesDto {
41
+ // The names are the client's file names; the resource validates each one (characters,
42
+ // length, extension) when it builds the object key, which the decorators cannot.
43
+ names;
44
+ }
45
+ exports.SupportingFileNamesDto = SupportingFileNamesDto;
46
+ __decorate([
47
+ (0, class_validator_1.IsArray)({ message: 'names must be an array' }),
48
+ (0, class_validator_1.ArrayMinSize)(1, { message: 'At least one file is required' }),
49
+ (0, class_validator_1.ArrayMaxSize)(exports.MAX_FILES_PER_UPLOAD, { message: `At most ${exports.MAX_FILES_PER_UPLOAD} files per upload` }),
50
+ (0, class_validator_1.IsString)({ each: true, message: 'Each file name must be a string' }),
51
+ (0, class_validator_1.IsNotEmpty)({ each: true, message: 'A file name cannot be empty' }),
52
+ __metadata("design:type", Array)
53
+ ], SupportingFileNamesDto.prototype, "names", void 0);
@@ -1,5 +1,4 @@
1
- import type { SupportingFileEntity, SupportingFilePresignedUrl } from '@managemint-solutions/entities/supporting-files';
2
- import type { UploadSupportingFilesDto } from '@managemint-solutions/entities/supporting-files/dto';
1
+ import type { SupportingFileEntity, SupportingFilePresignedUrl, SupportingFileUploadTarget } from '@managemint-solutions/entities/supporting-files';
3
2
  import { SupportingFilesEntityTypes } from '@managemint-solutions/entities/supporting-files/enum';
4
3
  import type { AuthContext } from '../auth';
5
4
  import { type AuditResource } from '../audit';
@@ -13,6 +12,24 @@ export declare class SupportingFilesResource {
13
12
  constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
14
13
  getPresignedUrl(supportingFileId: string): Promise<SupportingFilePresignedUrl>;
15
14
  list(entity: SupportingFilesEntityTypes, entityId: string): Promise<SupportingFileEntity[]>;
16
- upload(entity: SupportingFilesEntityTypes, entityId: string, input: UploadSupportingFilesDto): Promise<SupportingFileEntity[]>;
15
+ /**
16
+ * Where the caller puts each file: one signed upload URL per name, in request order, valid
17
+ * for two hours, into the organization's prefix. Storage checks the bucket's INSERT policy
18
+ * here, as the caller. An existing object of the same name is replaced - the latest version
19
+ * of a document wins.
20
+ */
21
+ createUploadUrls(entity: SupportingFilesEntityTypes, entityId: string, names: string[]): Promise<SupportingFileUploadTarget[]>;
22
+ /**
23
+ * Records the files the caller has put in the bucket. A name whose object is not there is
24
+ * refused, so no row ever points at nothing; a name already recorded keeps its row (its
25
+ * object was refreshed in place). Returns the record's whole list.
26
+ */
27
+ record(entity: SupportingFilesEntityTypes, entityId: string, names: string[]): Promise<SupportingFileEntity[]>;
17
28
  remove(supportingFileId: string): Promise<void>;
29
+ /**
30
+ * Each client name validated once, and the object key built from that same split - so what
31
+ * `createUploadUrls` mints, `record` checks and `getPresignedUrl` and `remove` rebuild later
32
+ * are one key. A converted lead takes no new files.
33
+ */
34
+ private prepare;
18
35
  }
@@ -27,8 +27,14 @@ __exportStar(require("./paths"), exports);
27
27
  const BUCKET = 'supporting-files';
28
28
  /** How long a presigned download URL stays valid. */
29
29
  const SIGNED_URL_TTL_SECONDS = 300;
30
- /** Uploads run concurrently in batches of this size. */
31
- const UPLOAD_BATCH_SIZE = 5;
30
+ /** Storage calls per file (minting an upload URL, checking an object) run concurrently in batches of this size. */
31
+ const BATCH_SIZE = 5;
32
+ /**
33
+ * The only extensions a supporting file may carry - what the portal offers. The bytes never
34
+ * pass through the api, so this check on the name is the type gate; the bucket's
35
+ * `file_size_limit` is the size gate.
36
+ */
37
+ const ALLOWED_EXTENSIONS = new Set(Object.values(enum_1.SupportingFilesExtensions));
32
38
  /** The `name`/`extension` pair rendered back into the file name it was split from. */
33
39
  const fileNameOf = (file) => file.extension ? `${file.name}.${file.extension}` : file.name;
34
40
  // The public contract is the shared SupportingFileEntity from @managemint-solutions/entities; the
@@ -38,6 +44,10 @@ const fileNameOf = (file) => file.extension ? `${file.name}.${file.extension}` :
38
44
  // Object keys are `organizationId/entityType/entityId/file` - the organization leads so a storage
39
45
  // policy can scope by tenant. A data migration renames the pre-existing objects, which have no
40
46
  // organization segment, into that prefix.
47
+ //
48
+ // An upload is two calls: `createUploadUrls` hands the caller a signed upload URL per file, the
49
+ // caller puts each file's bytes straight into the bucket, and `record` writes the rows for what
50
+ // landed. The bytes never pass through here - a Vercel function body is capped at 4.5 MB.
41
51
  class SupportingFilesResource {
42
52
  supabase;
43
53
  auth;
@@ -78,9 +88,48 @@ class SupportingFilesResource {
78
88
  throw (0, errors_1.mapPostgrestError)(error);
79
89
  return data;
80
90
  }
81
- async upload(entity, entityId, input) {
82
- if (entity === enum_1.SupportingFilesEntityTypes.LEAD) {
83
- await (0, won_1.refuseWonLead)(this.supabase, this.auth.organization_id, entityId);
91
+ /**
92
+ * Where the caller puts each file: one signed upload URL per name, in request order, valid
93
+ * for two hours, into the organization's prefix. Storage checks the bucket's INSERT policy
94
+ * here, as the caller. An existing object of the same name is replaced - the latest version
95
+ * of a document wins.
96
+ */
97
+ async createUploadUrls(entity, entityId, names) {
98
+ const files = await this.prepare(entity, entityId, names);
99
+ const targets = [];
100
+ for (let index = 0; index < files.length; index += BATCH_SIZE) {
101
+ targets.push(...(await Promise.all(files.slice(index, index + BATCH_SIZE).map(async ({ name, extension, path }) => {
102
+ const { data, error } = await this.supabase.storage
103
+ .from(BUCKET)
104
+ .createSignedUploadUrl(path, { upsert: true });
105
+ if (error) {
106
+ throw (0, errors_1.mapStorageError)(error, `Failed to prepare upload: ${fileNameOf({ name, extension })}`);
107
+ }
108
+ return { name, extension, url: data.signedUrl };
109
+ }))));
110
+ }
111
+ return targets;
112
+ }
113
+ /**
114
+ * Records the files the caller has put in the bucket. A name whose object is not there is
115
+ * refused, so no row ever points at nothing; a name already recorded keeps its row (its
116
+ * object was refreshed in place). Returns the record's whole list.
117
+ */
118
+ async record(entity, entityId, names) {
119
+ const files = await this.prepare(entity, entityId, names);
120
+ for (let index = 0; index < files.length; index += BATCH_SIZE) {
121
+ await Promise.all(files.slice(index, index + BATCH_SIZE).map(async ({ name, extension, path }) => {
122
+ // A missing object comes back as `data: false` (storage-js keeps the 404 in `error`);
123
+ // any other failure is thrown by storage-js itself.
124
+ const { data: present } = await this.supabase.storage.from(BUCKET).exists(path);
125
+ if (!present) {
126
+ throw new errors_1.SupabaseClientError({
127
+ status: 400,
128
+ error: 'Storage Error',
129
+ message: `File was not uploaded: ${fileNameOf({ name, extension })}`,
130
+ });
131
+ }
132
+ }));
84
133
  }
85
134
  const { data: existing, error: existingError } = await this.supabase
86
135
  .from('supporting_files')
@@ -91,26 +140,7 @@ class SupportingFilesResource {
91
140
  if (existingError)
92
141
  throw (0, errors_1.mapPostgrestError)(existingError);
93
142
  const existingFileNames = existing.map(fileNameOf);
94
- // Each client name is validated once here, and the object key, the duplicate check and the
95
- // row are all built from that same split - so what is stored is what presign and remove
96
- // rebuild later.
97
- const uploads = input.files.map((file) => ({
98
- file,
99
- ...(0, paths_1.splitFileName)((0, paths_1.sanitizeFileName)(file.name)),
100
- }));
101
- // Every file is written to the bucket, duplicates included - an upsert refreshes the object.
102
- for (let index = 0; index < uploads.length; index += UPLOAD_BATCH_SIZE) {
103
- await Promise.all(uploads.slice(index, index + UPLOAD_BATCH_SIZE).map(async ({ file, name, extension }) => {
104
- const { error } = await this.supabase.storage
105
- .from(BUCKET)
106
- .upload((0, paths_1.storagePath)(this.auth.organization_id, entity, entityId, name, extension), file, { cacheControl: '3600', upsert: true });
107
- if (error) {
108
- throw (0, errors_1.mapStorageError)(error, `Failed to upload file: ${fileNameOf({ name, extension })}`);
109
- }
110
- }));
111
- }
112
- // Only files without an existing row get one - the rest were refreshed in place above.
113
- const newFiles = uploads.filter((file) => !existingFileNames.includes(fileNameOf(file)));
143
+ const newFiles = files.filter((file) => !existingFileNames.includes(fileNameOf(file)));
114
144
  if (newFiles.length > 0) {
115
145
  await this.table.insertMany(newFiles.map(({ name, extension }) => ({
116
146
  name,
@@ -140,5 +170,30 @@ class SupportingFilesResource {
140
170
  throw (0, errors_1.mapStorageError)(storageError, `Failed to delete file: ${fileName}`);
141
171
  }
142
172
  }
173
+ /**
174
+ * Each client name validated once, and the object key built from that same split - so what
175
+ * `createUploadUrls` mints, `record` checks and `getPresignedUrl` and `remove` rebuild later
176
+ * are one key. A converted lead takes no new files.
177
+ */
178
+ async prepare(entity, entityId, names) {
179
+ if (entity === enum_1.SupportingFilesEntityTypes.LEAD) {
180
+ await (0, won_1.refuseWonLead)(this.supabase, this.auth.organization_id, entityId);
181
+ }
182
+ return names.map((fileName) => {
183
+ const { name, extension } = (0, paths_1.splitFileName)((0, paths_1.sanitizeFileName)(fileName));
184
+ if (!ALLOWED_EXTENSIONS.has(extension.toLowerCase())) {
185
+ throw new errors_1.SupabaseClientError({
186
+ status: 400,
187
+ error: 'Storage Error',
188
+ message: `Unsupported file type: ${fileNameOf({ name, extension })}`,
189
+ });
190
+ }
191
+ return {
192
+ name,
193
+ extension: extension,
194
+ path: (0, paths_1.storagePath)(this.auth.organization_id, entity, entityId, name, extension),
195
+ };
196
+ });
197
+ }
143
198
  }
144
199
  exports.SupportingFilesResource = SupportingFilesResource;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@managemint-solutions/sdk",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
4
4
  "description": "Typed Supabase data-access SDK for ManageMint Solutions",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Scott Bebington <scottbebington@gmail.com>",
@@ -51,7 +51,7 @@
51
51
  "testEnvironment": "node"
52
52
  },
53
53
  "dependencies": {
54
- "@managemint-solutions/entities": "^1.32.0"
54
+ "@managemint-solutions/entities": "^1.34.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@nestjs/common": "^11.0.0",