@managemint-solutions/sdk 0.50.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
@@ -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
@@ -8,8 +8,8 @@
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
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.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 = 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 = 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; } });
@@ -91,6 +91,7 @@ Object.defineProperty(exports, "UserIdDto", { enumerable: true, get: function ()
91
91
  var supporting_files_1 = require("./supporting-files");
92
92
  Object.defineProperty(exports, "GetSupportingFilesDto", { enumerable: true, get: function () { return supporting_files_1.GetSupportingFilesDto; } });
93
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; } });
94
95
  var clients_1 = require("./clients");
95
96
  Object.defineProperty(exports, "ClientIdDto", { enumerable: true, get: function () { return clients_1.ClientIdDto; } });
96
97
  Object.defineProperty(exports, "CreateClientDto", { enumerable: true, get: function () { return clients_1.CreateClientDto; } });
@@ -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.50.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.33.0"
54
+ "@managemint-solutions/entities": "^1.34.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@nestjs/common": "^11.0.0",