@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 +2 -2
- package/dist/index.js +5 -3
- package/dist/statuses/dto.d.ts +4 -1
- package/dist/statuses/dto.js +13 -1
- package/dist/statuses/index.d.ts +10 -1
- package/dist/statuses/index.js +51 -2
- package/dist/supporting-files/dto.d.ts +6 -1
- package/dist/supporting-files/dto.js +18 -3
- package/dist/supporting-files/index.d.ts +20 -3
- package/dist/supporting-files/index.js +80 -25
- package/package.json +2 -2
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.
|
|
11
|
-
exports.
|
|
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; } });
|
package/dist/statuses/dto.d.ts
CHANGED
|
@@ -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;
|
package/dist/statuses/dto.js
CHANGED
|
@@ -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)
|
package/dist/statuses/index.d.ts
CHANGED
|
@@ -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>;
|
package/dist/statuses/index.js
CHANGED
|
@@ -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.
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
31
|
-
const
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
54
|
+
"@managemint-solutions/entities": "^1.34.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@nestjs/common": "^11.0.0",
|