@managemint-solutions/sdk 0.38.0 → 0.38.1
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/README.md +27 -27
- package/dist/audit/dto.js +1 -1
- package/dist/audit/index.d.ts +12 -12
- package/dist/audit/index.js +9 -9
- package/dist/auth-client/index.d.ts +2 -2
- package/dist/auth-client/index.js +2 -2
- package/dist/auth.d.ts +3 -3
- package/dist/auth.js +3 -3
- package/dist/billing/dto.js +1 -1
- package/dist/billing/index.d.ts +4 -4
- package/dist/billing/index.js +5 -5
- package/dist/billing/invoices/dto.js +1 -1
- package/dist/billing/invoices/index.d.ts +6 -6
- package/dist/billing/invoices/index.js +7 -7
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/clients/index.d.ts +1 -1
- package/dist/clients/index.js +1 -1
- package/dist/configs/dto.js +3 -3
- package/dist/configs/index.d.ts +3 -3
- package/dist/configs/index.js +3 -3
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +2 -2
- package/dist/feature-requests/dto.js +1 -1
- package/dist/feature-requests/index.d.ts +3 -3
- package/dist/feature-requests/index.js +2 -2
- package/dist/index.js +2 -2
- package/dist/nest/index.d.ts +5 -5
- package/dist/nest/index.js +4 -4
- package/dist/notifications/index.d.ts +8 -8
- package/dist/notifications/index.js +8 -8
- package/dist/organization-invites/index.d.ts +3 -3
- package/dist/organization-invites/index.js +2 -2
- package/dist/organization-payment-methods/index.d.ts +3 -3
- package/dist/organization-payment-methods/index.js +3 -3
- package/dist/organizations/index.d.ts +7 -7
- package/dist/organizations/index.js +7 -7
- package/dist/permissions/index.d.ts +6 -6
- package/dist/permissions/index.js +6 -6
- package/dist/pricebooks/index.d.ts +2 -2
- package/dist/pricebooks/index.js +3 -3
- package/dist/projects/dto.js +2 -2
- package/dist/projects/index.d.ts +1 -1
- package/dist/projects/index.js +2 -2
- package/dist/service-client/index.d.ts +5 -5
- package/dist/service-client/index.js +5 -5
- package/dist/support/dto.js +1 -1
- package/dist/supporting-files/dto.js +1 -1
- package/dist/supporting-files/index.js +5 -5
- package/dist/supporting-files/paths.d.ts +3 -3
- package/dist/supporting-files/paths.js +4 -4
- package/dist/tasks/dto.js +4 -4
- package/dist/tasks/index.d.ts +1 -1
- package/dist/tasks/index.js +2 -2
- package/dist/transforms.d.ts +6 -6
- package/dist/transforms.js +8 -8
- package/dist/users/dto.d.ts +3 -3
- package/dist/users/dto.js +4 -4
- package/dist/users/index.d.ts +2 -2
- package/dist/users/index.js +2 -2
- package/dist/users/paths.d.ts +2 -2
- package/dist/users/paths.js +2 -2
- package/dist/webhook-events/index.d.ts +3 -3
- package/dist/webhook-events/index.js +3 -3
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ export type PermissionFlags = {
|
|
|
9
9
|
} & Record<string, boolean>;
|
|
10
10
|
/**
|
|
11
11
|
* The `permissions` table. Rows are created by the `on_auth_user_created` trigger and deleted
|
|
12
|
-
* with the user
|
|
12
|
+
* with the user - INSERT and DELETE are `false` for `authenticated` - so this resource only
|
|
13
13
|
* reads and updates.
|
|
14
14
|
*/
|
|
15
15
|
export declare class PermissionsResource {
|
|
@@ -18,8 +18,8 @@ export declare class PermissionsResource {
|
|
|
18
18
|
private readonly table;
|
|
19
19
|
constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
|
|
20
20
|
/**
|
|
21
|
-
* The whole permissions row. `*` rather than a select string
|
|
22
|
-
* two role flags and all 78 permission flags
|
|
21
|
+
* The whole permissions row. `*` rather than a select string - the alternative is naming the
|
|
22
|
+
* two role flags and all 78 permission flags - so the internal `id` is dropped on the way out.
|
|
23
23
|
*/
|
|
24
24
|
getForUser(userId: string): Promise<PermissionsEntity>;
|
|
25
25
|
/**
|
|
@@ -28,13 +28,13 @@ export declare class PermissionsResource {
|
|
|
28
28
|
* guard treats as a denial.
|
|
29
29
|
*
|
|
30
30
|
* `flag` is interpolated into the select string, so it is checked against the shape a column
|
|
31
|
-
* name can take first
|
|
31
|
+
* name can take first - a flag never comes from a request, only from a route's own decorator,
|
|
32
32
|
* so anything else is a bug rather than a bad request.
|
|
33
33
|
*/
|
|
34
34
|
flagsFor(userId: string, flag: string): Promise<PermissionFlags | null>;
|
|
35
|
-
/** Whether a user owns the organization
|
|
35
|
+
/** Whether a user owns the organization - the guard behind "you cannot deactivate the owner". */
|
|
36
36
|
isOwner(userId: string): Promise<boolean>;
|
|
37
|
-
/** Who owns the organization. Null when there is no owner row yet
|
|
37
|
+
/** Who owns the organization. Null when there is no owner row yet - signup is mid-flight. */
|
|
38
38
|
ownerUserId(): Promise<string | null>;
|
|
39
39
|
/**
|
|
40
40
|
* Permissions are keyed by `user_id` on the wire but audited by the row's own `mms_id`, so
|
|
@@ -25,7 +25,7 @@ const PERMISSIONS_TABLE = {
|
|
|
25
25
|
};
|
|
26
26
|
/**
|
|
27
27
|
* The `permissions` table. Rows are created by the `on_auth_user_created` trigger and deleted
|
|
28
|
-
* with the user
|
|
28
|
+
* with the user - INSERT and DELETE are `false` for `authenticated` - so this resource only
|
|
29
29
|
* reads and updates.
|
|
30
30
|
*/
|
|
31
31
|
class PermissionsResource {
|
|
@@ -38,8 +38,8 @@ class PermissionsResource {
|
|
|
38
38
|
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, PERMISSIONS_TABLE);
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
* The whole permissions row. `*` rather than a select string
|
|
42
|
-
* two role flags and all 78 permission flags
|
|
41
|
+
* The whole permissions row. `*` rather than a select string - the alternative is naming the
|
|
42
|
+
* two role flags and all 78 permission flags - so the internal `id` is dropped on the way out.
|
|
43
43
|
*/
|
|
44
44
|
async getForUser(userId) {
|
|
45
45
|
const { data, error } = await this.supabase
|
|
@@ -58,7 +58,7 @@ class PermissionsResource {
|
|
|
58
58
|
* guard treats as a denial.
|
|
59
59
|
*
|
|
60
60
|
* `flag` is interpolated into the select string, so it is checked against the shape a column
|
|
61
|
-
* name can take first
|
|
61
|
+
* name can take first - a flag never comes from a request, only from a route's own decorator,
|
|
62
62
|
* so anything else is a bug rather than a bad request.
|
|
63
63
|
*/
|
|
64
64
|
async flagsFor(userId, flag) {
|
|
@@ -79,7 +79,7 @@ class PermissionsResource {
|
|
|
79
79
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
80
80
|
return (data ?? null);
|
|
81
81
|
}
|
|
82
|
-
/** Whether a user owns the organization
|
|
82
|
+
/** Whether a user owns the organization - the guard behind "you cannot deactivate the owner". */
|
|
83
83
|
async isOwner(userId) {
|
|
84
84
|
const { data, error } = await this.supabase
|
|
85
85
|
.from('permissions')
|
|
@@ -91,7 +91,7 @@ class PermissionsResource {
|
|
|
91
91
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
92
92
|
return data?.is_owner === true;
|
|
93
93
|
}
|
|
94
|
-
/** Who owns the organization. Null when there is no owner row yet
|
|
94
|
+
/** Who owns the organization. Null when there is no owner row yet - signup is mid-flight. */
|
|
95
95
|
async ownerUserId() {
|
|
96
96
|
const { data, error } = await this.supabase
|
|
97
97
|
.from('permissions')
|
|
@@ -2,7 +2,7 @@ import type { SupabaseClient as SupabaseJsClient } from '@supabase/supabase-js';
|
|
|
2
2
|
import type { PricebookWithModules } from '@managemint-solutions/entities/modules';
|
|
3
3
|
/**
|
|
4
4
|
* The public price catalog. `pricebooks`, `pricebook_modules` and `billing_modules` are readable
|
|
5
|
-
* by `anon` by design
|
|
5
|
+
* by `anon` by design - unauthenticated signup prices against them - so this resource takes no
|
|
6
6
|
* auth context and scopes by nothing.
|
|
7
7
|
*/
|
|
8
8
|
export declare class PricebooksResource {
|
|
@@ -13,7 +13,7 @@ export declare class PricebooksResource {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* What the given modules cost per month, in minor units: every module's unit price summed and
|
|
16
|
-
* multiplied by the seat count. A module the pricebook does not price is a 400
|
|
16
|
+
* multiplied by the seat count. A module the pricebook does not price is a 400 - the caller
|
|
17
17
|
* asked for something that cannot be billed.
|
|
18
18
|
*/
|
|
19
19
|
export declare const monthlyTotalMinor: (pricebook: PricebookWithModules, moduleKeys: string[], seatCount: number) => number;
|
package/dist/pricebooks/index.js
CHANGED
|
@@ -19,7 +19,7 @@ const PRICEBOOK_SELECT = `
|
|
|
19
19
|
)
|
|
20
20
|
)
|
|
21
21
|
`;
|
|
22
|
-
// A pricebook_modules row whose catalog entry did not come back leaves `module_key` undefined
|
|
22
|
+
// A pricebook_modules row whose catalog entry did not come back leaves `module_key` undefined -
|
|
23
23
|
// the cast keeps the mapping honest about that rather than inventing a key.
|
|
24
24
|
const toPricebookWithModules = (row) => ({
|
|
25
25
|
mms_id: row.mms_id,
|
|
@@ -35,7 +35,7 @@ const toPricebookWithModules = (row) => ({
|
|
|
35
35
|
});
|
|
36
36
|
/**
|
|
37
37
|
* The public price catalog. `pricebooks`, `pricebook_modules` and `billing_modules` are readable
|
|
38
|
-
* by `anon` by design
|
|
38
|
+
* by `anon` by design - unauthenticated signup prices against them - so this resource takes no
|
|
39
39
|
* auth context and scopes by nothing.
|
|
40
40
|
*/
|
|
41
41
|
class PricebooksResource {
|
|
@@ -67,7 +67,7 @@ class PricebooksResource {
|
|
|
67
67
|
exports.PricebooksResource = PricebooksResource;
|
|
68
68
|
/**
|
|
69
69
|
* What the given modules cost per month, in minor units: every module's unit price summed and
|
|
70
|
-
* multiplied by the seat count. A module the pricebook does not price is a 400
|
|
70
|
+
* multiplied by the seat count. A module the pricebook does not price is a 400 - the caller
|
|
71
71
|
* asked for something that cannot be billed.
|
|
72
72
|
*/
|
|
73
73
|
const monthlyTotalMinor = (pricebook, moduleKeys, seatCount) => {
|
package/dist/projects/dto.js
CHANGED
|
@@ -196,7 +196,7 @@ __decorate([
|
|
|
196
196
|
(0, class_validator_1.IsOptional)(),
|
|
197
197
|
(0, validators_1.IsNullable)()
|
|
198
198
|
// Still v4: a manager is a `users.mms_id`, and that column carries the GoTrue auth uid the
|
|
199
|
-
// signup trigger copies out of `auth.users.id`
|
|
199
|
+
// signup trigger copies out of `auth.users.id` - the table's uuidv7 default never fires.
|
|
200
200
|
,
|
|
201
201
|
(0, class_validator_1.IsUUID)('4', { message: 'Invalid manager ID format' }),
|
|
202
202
|
__metadata("design:type", Object)
|
|
@@ -256,7 +256,7 @@ __decorate([
|
|
|
256
256
|
__decorate([
|
|
257
257
|
(0, class_validator_1.IsOptional)(),
|
|
258
258
|
(0, validators_1.IsNullable)()
|
|
259
|
-
// Still v4
|
|
259
|
+
// Still v4 - a manager is a `users.mms_id`, the GoTrue auth uid (see CreateProjectDto).
|
|
260
260
|
,
|
|
261
261
|
(0, class_validator_1.IsUUID)('4', { message: 'Invalid manager ID format' }),
|
|
262
262
|
__metadata("design:type", Object)
|
package/dist/projects/index.d.ts
CHANGED
|
@@ -17,6 +17,6 @@ export declare class ProjectsResource {
|
|
|
17
17
|
archive(projectId: string): Promise<ProjectEntity>;
|
|
18
18
|
unarchive(projectId: string): Promise<ProjectEntity>;
|
|
19
19
|
restore(projectId: string): Promise<ProjectEntity>;
|
|
20
|
-
/** Soft delete
|
|
20
|
+
/** Soft delete - the row stays, flagged `deleted`, and `restore()` brings it back. */
|
|
21
21
|
remove(projectId: string): Promise<void>;
|
|
22
22
|
}
|
package/dist/projects/index.js
CHANGED
|
@@ -126,7 +126,7 @@ class ProjectsResource {
|
|
|
126
126
|
}
|
|
127
127
|
// Excluding statuses has to be spelled out as "no status OR not one of these": PostgREST can
|
|
128
128
|
// only filter a parent row by an embedded column with `!inner`, which would drop projects that
|
|
129
|
-
// have no status at all
|
|
129
|
+
// have no status at all - and a project with no status is not complete, so it belongs in the
|
|
130
130
|
// default view. SQL's `NOT IN` drops nulls for the same reason, hence the explicit null arm.
|
|
131
131
|
// The ids are `@IsUUID('7')`-validated by the DTO, so interpolating them is safe. Repeated
|
|
132
132
|
// `or` params are ANDed by PostgREST, so this composes with the search filter above.
|
|
@@ -205,7 +205,7 @@ class ProjectsResource {
|
|
|
205
205
|
await this.table.transition(projectId, enum_1.AuditTrailAction.RESTORE, (0, transforms_1.restoredPatch)(this.auth.mms_id));
|
|
206
206
|
return this.getById(projectId);
|
|
207
207
|
}
|
|
208
|
-
/** Soft delete
|
|
208
|
+
/** Soft delete - the row stays, flagged `deleted`, and `restore()` brings it back. */
|
|
209
209
|
async remove(projectId) {
|
|
210
210
|
await this.table.transition(projectId, enum_1.AuditTrailAction.DELETE, (0, transforms_1.deletedPatch)(this.auth.mms_id));
|
|
211
211
|
}
|
|
@@ -13,14 +13,14 @@ import { UsersResource } from '../users';
|
|
|
13
13
|
import { WebhookEventsResource } from '../webhook-events';
|
|
14
14
|
export type SupabaseServiceClientConfig = {
|
|
15
15
|
url: string;
|
|
16
|
-
/** Service-role key
|
|
16
|
+
/** Service-role key - every query bypasses RLS. */
|
|
17
17
|
serviceRoleKey: string;
|
|
18
18
|
/** Where a failed audit insert is reported. Defaults to `console.error`. */
|
|
19
19
|
onAuditError?: AuditErrorHandler;
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
22
|
* The organization-scoped half of the service client. Everything here is attributed to one
|
|
23
|
-
* `AuditActor`, so writes land in `audit_logs` the same way a caller's own would
|
|
23
|
+
* `AuditActor`, so writes land in `audit_logs` the same way a caller's own would - with
|
|
24
24
|
* `mms_id: null` when the actor is the system (a webhook, a queue worker, provisioning).
|
|
25
25
|
*/
|
|
26
26
|
export declare class SupabaseOrganizationScope {
|
|
@@ -36,7 +36,7 @@ export declare class SupabaseOrganizationScope {
|
|
|
36
36
|
readonly users: UsersResource;
|
|
37
37
|
constructor(supabase: TypedSupabaseClient, actor: AuditActor, onAuditError?: AuditErrorHandler);
|
|
38
38
|
/**
|
|
39
|
-
* An audited table bound to this organization and actor
|
|
39
|
+
* An audited table bound to this organization and actor - the shape the api hand-rolls today
|
|
40
40
|
* as a local `auditedTables(client, organizationId)` factory in billing, provisioning,
|
|
41
41
|
* subscriptions and both webhook handler services.
|
|
42
42
|
*/
|
|
@@ -46,7 +46,7 @@ export declare class SupabaseOrganizationScope {
|
|
|
46
46
|
* The client for work that has no caller and no RLS context to run under: inbound webhooks
|
|
47
47
|
* (Paystack, Resend, the Supabase `send_email` hook), the notification queue worker, unauthenticated
|
|
48
48
|
* signup, the website's feature-request intake, and the writes RLS reserves for the service role
|
|
49
|
-
* outright
|
|
49
|
+
* outright - the ledger, the outbox, the webhook event logs, `organizations` INSERT, `permissions`
|
|
50
50
|
* INSERT/DELETE.
|
|
51
51
|
*
|
|
52
52
|
* It decodes no JWT, because there is none. Cross-org lookups (find the organization behind a
|
|
@@ -64,7 +64,7 @@ export declare class SupabaseServiceClient {
|
|
|
64
64
|
readonly resendEvents: WebhookEventsResource;
|
|
65
65
|
constructor(config: SupabaseServiceClientConfig);
|
|
66
66
|
/**
|
|
67
|
-
* Scope to one organization once it is known. `actorMmsId` stays `null` for system work
|
|
67
|
+
* Scope to one organization once it is known. `actorMmsId` stays `null` for system work -
|
|
68
68
|
* `audit_logs.created_by` is nullable precisely so an unattributed write is still recorded.
|
|
69
69
|
*/
|
|
70
70
|
forOrganization(organizationId: string, actorMmsId?: string | null): SupabaseOrganizationScope;
|
|
@@ -18,7 +18,7 @@ const users_1 = require("../users");
|
|
|
18
18
|
const webhook_events_1 = require("../webhook-events");
|
|
19
19
|
/**
|
|
20
20
|
* The organization-scoped half of the service client. Everything here is attributed to one
|
|
21
|
-
* `AuditActor`, so writes land in `audit_logs` the same way a caller's own would
|
|
21
|
+
* `AuditActor`, so writes land in `audit_logs` the same way a caller's own would - with
|
|
22
22
|
* `mms_id: null` when the actor is the system (a webhook, a queue worker, provisioning).
|
|
23
23
|
*/
|
|
24
24
|
class SupabaseOrganizationScope {
|
|
@@ -45,7 +45,7 @@ class SupabaseOrganizationScope {
|
|
|
45
45
|
this.users = new users_1.UsersResource(supabase, actor, this.audit);
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
* An audited table bound to this organization and actor
|
|
48
|
+
* An audited table bound to this organization and actor - the shape the api hand-rolls today
|
|
49
49
|
* as a local `auditedTables(client, organizationId)` factory in billing, provisioning,
|
|
50
50
|
* subscriptions and both webhook handler services.
|
|
51
51
|
*/
|
|
@@ -58,7 +58,7 @@ exports.SupabaseOrganizationScope = SupabaseOrganizationScope;
|
|
|
58
58
|
* The client for work that has no caller and no RLS context to run under: inbound webhooks
|
|
59
59
|
* (Paystack, Resend, the Supabase `send_email` hook), the notification queue worker, unauthenticated
|
|
60
60
|
* signup, the website's feature-request intake, and the writes RLS reserves for the service role
|
|
61
|
-
* outright
|
|
61
|
+
* outright - the ledger, the outbox, the webhook event logs, `organizations` INSERT, `permissions`
|
|
62
62
|
* INSERT/DELETE.
|
|
63
63
|
*
|
|
64
64
|
* It decodes no JWT, because there is none. Cross-org lookups (find the organization behind a
|
|
@@ -85,7 +85,7 @@ class SupabaseServiceClient {
|
|
|
85
85
|
this.featureRequests = new feature_requests_1.FeatureRequestsResource(this.supabase);
|
|
86
86
|
this.configs = new configs_1.ConfigsResource(this.supabase);
|
|
87
87
|
this.signup = new organizations_1.OrganizationSignupResource(this.supabase, (organizationId) => this.forOrganization(organizationId).audit);
|
|
88
|
-
// The outbox spans organizations
|
|
88
|
+
// The outbox spans organizations - the row it is about carries its own - so it takes a
|
|
89
89
|
// factory and scopes per write rather than being built inside one organization's scope.
|
|
90
90
|
this.notifications = new notifications_1.NotificationsOutboxResource(this.supabase, (organizationId) => this.forOrganization(organizationId).table({
|
|
91
91
|
table: 'notifications',
|
|
@@ -93,7 +93,7 @@ class SupabaseServiceClient {
|
|
|
93
93
|
}));
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
|
-
* Scope to one organization once it is known. `actorMmsId` stays `null` for system work
|
|
96
|
+
* Scope to one organization once it is known. `actorMmsId` stays `null` for system work -
|
|
97
97
|
* `audit_logs.created_by` is nullable precisely so an unattributed write is still recorded.
|
|
98
98
|
*/
|
|
99
99
|
forOrganization(organizationId, actorMmsId = null) {
|
package/dist/support/dto.js
CHANGED
|
@@ -18,7 +18,7 @@ const validators_1 = require("../validators");
|
|
|
18
18
|
// wire contract cannot drift.
|
|
19
19
|
/**
|
|
20
20
|
* Whitespace on either side of a pasted value is not part of the answer. Trimming here rather
|
|
21
|
-
* than in the api means the length rules below measure what will actually be sent
|
|
21
|
+
* than in the api means the length rules below measure what will actually be sent - a name of
|
|
22
22
|
* three spaces is empty, not three characters long.
|
|
23
23
|
*/
|
|
24
24
|
const Trim = () => (0, class_transformer_1.Transform)(({ value }) => (typeof value === 'string' ? value.trim() : value), {
|
|
@@ -14,7 +14,7 @@ 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
16
|
// Each class `implements` its entities type so the wire contract cannot drift. There is no class
|
|
17
|
-
// for the upload payload
|
|
17
|
+
// for the upload payload - it arrives as multipart and is not validated by class-validator.
|
|
18
18
|
class SupportingFileIdDto {
|
|
19
19
|
supportingFileId;
|
|
20
20
|
}
|
|
@@ -33,7 +33,7 @@ const fileNameOf = (file) => file.extension ? `${file.name}.${file.extension}` :
|
|
|
33
33
|
// casts at the returns declare the shape the select strings below produce (the columns are
|
|
34
34
|
// defined in the api's supabase/migrations).
|
|
35
35
|
//
|
|
36
|
-
// Object keys are `organizationId/entityType/entityId/file`
|
|
36
|
+
// Object keys are `organizationId/entityType/entityId/file` - the organization leads so a storage
|
|
37
37
|
// policy can scope by tenant. A data migration renames the pre-existing objects, which have no
|
|
38
38
|
// organization segment, into that prefix.
|
|
39
39
|
class SupportingFilesResource {
|
|
@@ -87,13 +87,13 @@ class SupportingFilesResource {
|
|
|
87
87
|
throw (0, errors_1.mapPostgrestError)(existingError);
|
|
88
88
|
const existingFileNames = existing.map(fileNameOf);
|
|
89
89
|
// Each client name is validated once here, and the object key, the duplicate check and the
|
|
90
|
-
// row are all built from that same split
|
|
90
|
+
// row are all built from that same split - so what is stored is what presign and remove
|
|
91
91
|
// rebuild later.
|
|
92
92
|
const uploads = input.files.map((file) => ({
|
|
93
93
|
file,
|
|
94
94
|
...(0, paths_1.splitFileName)((0, paths_1.sanitizeFileName)(file.name)),
|
|
95
95
|
}));
|
|
96
|
-
// Every file is written to the bucket, duplicates included
|
|
96
|
+
// Every file is written to the bucket, duplicates included - an upsert refreshes the object.
|
|
97
97
|
for (let index = 0; index < uploads.length; index += UPLOAD_BATCH_SIZE) {
|
|
98
98
|
await Promise.all(uploads.slice(index, index + UPLOAD_BATCH_SIZE).map(async ({ file, name, extension }) => {
|
|
99
99
|
const { error } = await this.supabase.storage
|
|
@@ -104,7 +104,7 @@ class SupportingFilesResource {
|
|
|
104
104
|
}
|
|
105
105
|
}));
|
|
106
106
|
}
|
|
107
|
-
// Only files without an existing row get one
|
|
107
|
+
// Only files without an existing row get one - the rest were refreshed in place above.
|
|
108
108
|
const newFiles = uploads.filter((file) => !existingFileNames.includes(fileNameOf(file)));
|
|
109
109
|
if (newFiles.length > 0) {
|
|
110
110
|
await this.table.insertMany(newFiles.map(({ name, extension }) => ({
|
|
@@ -120,7 +120,7 @@ class SupportingFilesResource {
|
|
|
120
120
|
return this.list(entity, entityId);
|
|
121
121
|
}
|
|
122
122
|
async remove(supportingFileId) {
|
|
123
|
-
// The row goes first
|
|
123
|
+
// The row goes first - the helper's pre-read hands back what the object path is built from.
|
|
124
124
|
const data = await this.table.remove(supportingFileId);
|
|
125
125
|
const { error: storageError } = await this.supabase.storage
|
|
126
126
|
.from(BUCKET)
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* The client-supplied file name, checked before it is allowed anywhere near an object key.
|
|
3
3
|
*
|
|
4
4
|
* The name is half of the key, so a separator or a `..` in it would let an upload escape its
|
|
5
|
-
* entity's folder
|
|
5
|
+
* entity's folder - and, now that the key starts with the organization, its tenant's folder.
|
|
6
6
|
* Anything that is not a plain file name is rejected outright rather than rewritten, so what the
|
|
7
7
|
* `supporting_files` row records is exactly what was stored.
|
|
8
8
|
*/
|
|
9
9
|
export declare const sanitizeFileName: (fileName: string) => string;
|
|
10
10
|
/**
|
|
11
|
-
* Splits an uploaded file name into the `name` / `extension` columns, on the *last* dot
|
|
11
|
+
* Splits an uploaded file name into the `name` / `extension` columns, on the *last* dot - so
|
|
12
12
|
* `archive.tar.gz` is name `archive.tar`, extension `gz`, and `storagePath` rebuilds the key it
|
|
13
13
|
* was stored under. A name with no dot has no extension.
|
|
14
14
|
*/
|
|
@@ -18,7 +18,7 @@ export declare const splitFileName: (fileName: string) => {
|
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
20
|
* Where an object lives in the `supporting-files` bucket. The organization leads the key so a
|
|
21
|
-
* storage policy can scope objects by tenant
|
|
21
|
+
* storage policy can scope objects by tenant - nothing below it is trusted for that.
|
|
22
22
|
*
|
|
23
23
|
* A data migration renames the existing objects (keyed `entity/entityId/file`) into their
|
|
24
24
|
* organization's prefix; keys written from here already carry it.
|
|
@@ -4,14 +4,14 @@ exports.storagePath = exports.splitFileName = exports.sanitizeFileName = void 0;
|
|
|
4
4
|
const errors_1 = require("../errors");
|
|
5
5
|
/** Longest object-key segment a file name may become. */
|
|
6
6
|
const MAX_FILE_NAME_LENGTH = 255;
|
|
7
|
-
/** What a file name may be made of
|
|
7
|
+
/** What a file name may be made of - no separators, no control characters, no leading dot. */
|
|
8
8
|
const SAFE_FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9 ._()\-]*$/;
|
|
9
9
|
const invalidFileName = () => new errors_1.SupabaseClientError({ status: 400, error: 'Storage Error', message: 'Invalid file name' });
|
|
10
10
|
/**
|
|
11
11
|
* The client-supplied file name, checked before it is allowed anywhere near an object key.
|
|
12
12
|
*
|
|
13
13
|
* The name is half of the key, so a separator or a `..` in it would let an upload escape its
|
|
14
|
-
* entity's folder
|
|
14
|
+
* entity's folder - and, now that the key starts with the organization, its tenant's folder.
|
|
15
15
|
* Anything that is not a plain file name is rejected outright rather than rewritten, so what the
|
|
16
16
|
* `supporting_files` row records is exactly what was stored.
|
|
17
17
|
*/
|
|
@@ -31,7 +31,7 @@ const sanitizeFileName = (fileName) => {
|
|
|
31
31
|
};
|
|
32
32
|
exports.sanitizeFileName = sanitizeFileName;
|
|
33
33
|
/**
|
|
34
|
-
* Splits an uploaded file name into the `name` / `extension` columns, on the *last* dot
|
|
34
|
+
* Splits an uploaded file name into the `name` / `extension` columns, on the *last* dot - so
|
|
35
35
|
* `archive.tar.gz` is name `archive.tar`, extension `gz`, and `storagePath` rebuilds the key it
|
|
36
36
|
* was stored under. A name with no dot has no extension.
|
|
37
37
|
*/
|
|
@@ -44,7 +44,7 @@ const splitFileName = (fileName) => {
|
|
|
44
44
|
exports.splitFileName = splitFileName;
|
|
45
45
|
/**
|
|
46
46
|
* Where an object lives in the `supporting-files` bucket. The organization leads the key so a
|
|
47
|
-
* storage policy can scope objects by tenant
|
|
47
|
+
* storage policy can scope objects by tenant - nothing below it is trusted for that.
|
|
48
48
|
*
|
|
49
49
|
* A data migration renames the existing objects (keyed `entity/entityId/file`) into their
|
|
50
50
|
* organization's prefix; keys written from here already carry it.
|
package/dist/tasks/dto.js
CHANGED
|
@@ -21,7 +21,7 @@ class TaskIdDto {
|
|
|
21
21
|
}
|
|
22
22
|
exports.TaskIdDto = TaskIdDto;
|
|
23
23
|
__decorate([
|
|
24
|
-
(0, class_validator_1.IsUUID)('7', { message: 'Invalid project ID format' }) // sic
|
|
24
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid project ID format' }) // sic - the api's message, kept for parity
|
|
25
25
|
,
|
|
26
26
|
__metadata("design:type", String)
|
|
27
27
|
], TaskIdDto.prototype, "taskId", void 0);
|
|
@@ -92,7 +92,7 @@ __decorate([
|
|
|
92
92
|
(0, class_validator_1.IsOptional)(),
|
|
93
93
|
(0, validators_1.IsNullable)()
|
|
94
94
|
// Still v4: the assignee is a `users.mms_id`, and that column carries the GoTrue auth uid the
|
|
95
|
-
// signup trigger copies out of `auth.users.id`
|
|
95
|
+
// signup trigger copies out of `auth.users.id` - the table's uuidv7 default never fires.
|
|
96
96
|
,
|
|
97
97
|
(0, class_validator_1.IsUUID)('4', { message: 'Invalid assigned to ID format' }),
|
|
98
98
|
__metadata("design:type", Object)
|
|
@@ -222,7 +222,7 @@ __decorate([
|
|
|
222
222
|
__decorate([
|
|
223
223
|
(0, class_validator_1.IsOptional)(),
|
|
224
224
|
(0, validators_1.IsNullable)()
|
|
225
|
-
// Still v4
|
|
225
|
+
// Still v4 - an assignee is a `users.mms_id`, the GoTrue auth uid (see GetTasksDto).
|
|
226
226
|
,
|
|
227
227
|
(0, class_validator_1.IsUUID)('4', { message: 'Invalid assigned to ID format' }),
|
|
228
228
|
__metadata("design:type", Object)
|
|
@@ -295,7 +295,7 @@ __decorate([
|
|
|
295
295
|
__decorate([
|
|
296
296
|
(0, class_validator_1.IsOptional)(),
|
|
297
297
|
(0, validators_1.IsNullable)()
|
|
298
|
-
// Still v4
|
|
298
|
+
// Still v4 - an assignee is a `users.mms_id`, the GoTrue auth uid (see GetTasksDto).
|
|
299
299
|
,
|
|
300
300
|
(0, class_validator_1.IsUUID)('4', { message: 'Invalid assigned to ID format' }),
|
|
301
301
|
__metadata("design:type", Object)
|
package/dist/tasks/index.d.ts
CHANGED
|
@@ -17,6 +17,6 @@ export declare class TasksResource {
|
|
|
17
17
|
archive(taskId: string): Promise<TaskEntity>;
|
|
18
18
|
unarchive(taskId: string): Promise<TaskEntity>;
|
|
19
19
|
restore(taskId: string): Promise<TaskEntity>;
|
|
20
|
-
/** Soft delete
|
|
20
|
+
/** Soft delete - the row stays, flagged `deleted`, and `restore()` brings it back. */
|
|
21
21
|
remove(taskId: string): Promise<void>;
|
|
22
22
|
}
|
package/dist/tasks/index.js
CHANGED
|
@@ -140,7 +140,7 @@ class TasksResource {
|
|
|
140
140
|
}
|
|
141
141
|
// Excluding statuses has to be spelled out as "no status OR not one of these": PostgREST can
|
|
142
142
|
// only filter a parent row by an embedded column with `!inner`, which would drop tasks that
|
|
143
|
-
// have no status at all
|
|
143
|
+
// have no status at all - and a task with no status is not complete, so it belongs in the
|
|
144
144
|
// default view. SQL's `NOT IN` drops nulls for the same reason, hence the explicit null arm.
|
|
145
145
|
// The ids are `@IsUUID('7')`-validated by the DTO, so interpolating them is safe. Repeated
|
|
146
146
|
// `or` params are ANDed by PostgREST, so this composes with the search filter above.
|
|
@@ -197,7 +197,7 @@ class TasksResource {
|
|
|
197
197
|
await this.table.transition(taskId, enum_1.AuditTrailAction.RESTORE, (0, transforms_1.restoredPatch)(this.auth.mms_id));
|
|
198
198
|
return this.getById(taskId);
|
|
199
199
|
}
|
|
200
|
-
/** Soft delete
|
|
200
|
+
/** Soft delete - the row stays, flagged `deleted`, and `restore()` brings it back. */
|
|
201
201
|
async remove(taskId) {
|
|
202
202
|
await this.table.transition(taskId, enum_1.AuditTrailAction.DELETE, (0, transforms_1.deletedPatch)(this.auth.mms_id));
|
|
203
203
|
}
|
package/dist/transforms.d.ts
CHANGED
|
@@ -2,24 +2,24 @@ import type { MmsList } from '@managemint-solutions/entities/common';
|
|
|
2
2
|
/**
|
|
3
3
|
* The `client_id` / `project_id` list filters accept either a plain UUID v7 or the base64-JSON
|
|
4
4
|
* option value the portal's selects send (`{ mms_id }`, or `{ id }` as a fallback). Anything that
|
|
5
|
-
* is not one of those
|
|
5
|
+
* is not one of those - including a value that decodes to JSON without a UUID v7 - cannot filter
|
|
6
6
|
* anything, so it becomes `null` and the filter is skipped.
|
|
7
7
|
*/
|
|
8
8
|
export declare const parseEntityIdFilter: (value: unknown) => string | null;
|
|
9
9
|
/**
|
|
10
10
|
* A `%value%` ilike pattern, double-quoted for PostgREST's `or=(...)` grammar so `,`, `(`, `)`,
|
|
11
11
|
* `.`, `"` and `\` in the search stay literal instead of being read as more filters. Inside the
|
|
12
|
-
* quotes only `"` and `\` need escaping, which is what PostgREST defines
|
|
12
|
+
* quotes only `"` and `\` need escaping, which is what PostgREST defines - the `\,` the list
|
|
13
13
|
* endpoints used to emit was never an escape at all. LIKE's own wildcards `%` and `_` are left
|
|
14
14
|
* alone and still match as wildcards; they only widen the caller's own search, never its scope.
|
|
15
15
|
*/
|
|
16
16
|
export declare const ilikeContains: (value: string) => string;
|
|
17
|
-
/** The `or=(...)` argument matching `search` against any of `columns`
|
|
17
|
+
/** The `or=(...)` argument matching `search` against any of `columns` - `name.ilike."%x%",…`. */
|
|
18
18
|
export declare const orIlike: (columns: readonly string[], search: string) => string;
|
|
19
19
|
/**
|
|
20
20
|
* `date` columns take an ISO string. The shared `CreateProjectDto`/`UpdateProjectDto` types
|
|
21
|
-
* declare `Date`, but over the wire the value is the validated `@IsDateString` string
|
|
22
|
-
* must reach Postgres byte-identical ('2026-04-22' is a plain date, not an instant)
|
|
21
|
+
* declare `Date`, but over the wire the value is the validated `@IsDateString` string - which
|
|
22
|
+
* must reach Postgres byte-identical ('2026-04-22' is a plain date, not an instant) - so only a
|
|
23
23
|
* real `Date` (a portal caller) is serialized.
|
|
24
24
|
*/
|
|
25
25
|
export declare const toDateInput: (value: Date | string | null | undefined) => string | null | undefined;
|
|
@@ -31,7 +31,7 @@ export declare const toDateInput: (value: Date | string | null | undefined) => s
|
|
|
31
31
|
export declare const USER_PROFILE_EMBED: "(\n mms_id,\n name,\n surname,\n email,\n profile_image\n )";
|
|
32
32
|
/**
|
|
33
33
|
* The `MmsList` envelope every paginated read returns. `rows` is `unknown` because each select
|
|
34
|
-
* string produces its own row shape
|
|
34
|
+
* string produces its own row shape - the caller's `T` is what declares it. `count` is
|
|
35
35
|
* PostgREST's estimate, so an empty page reports 0 total pages rather than 1.
|
|
36
36
|
*/
|
|
37
37
|
export declare const toMmsList: <T>(rows: unknown, page: number, pageSize: number, count: number | null) => MmsList<T>;
|
package/dist/transforms.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// select strings and their list results.
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.restoredPatch = exports.deletedPatch = exports.unarchivedPatch = exports.archivedPatch = exports.toMmsList = exports.USER_PROFILE_EMBED = exports.toDateInput = exports.orIlike = exports.ilikeContains = exports.parseEntityIdFilter = void 0;
|
|
6
|
-
// v7, to match the `mms_id` columns these filters compare against
|
|
6
|
+
// v7, to match the `mms_id` columns these filters compare against - `public.uuidv7()` is the
|
|
7
7
|
// default on every table. This is the gate in front of `@IsUUID('7')`, so a stricter version
|
|
8
8
|
// here would null the id out before validation ever saw it, and a looser one would let a value
|
|
9
9
|
// through that the DTO then rejects.
|
|
@@ -11,7 +11,7 @@ const UUID_V7 = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f
|
|
|
11
11
|
/**
|
|
12
12
|
* The `client_id` / `project_id` list filters accept either a plain UUID v7 or the base64-JSON
|
|
13
13
|
* option value the portal's selects send (`{ mms_id }`, or `{ id }` as a fallback). Anything that
|
|
14
|
-
* is not one of those
|
|
14
|
+
* is not one of those - including a value that decodes to JSON without a UUID v7 - cannot filter
|
|
15
15
|
* anything, so it becomes `null` and the filter is skipped.
|
|
16
16
|
*/
|
|
17
17
|
const parseEntityIdFilter = (value) => {
|
|
@@ -35,19 +35,19 @@ exports.parseEntityIdFilter = parseEntityIdFilter;
|
|
|
35
35
|
/**
|
|
36
36
|
* A `%value%` ilike pattern, double-quoted for PostgREST's `or=(...)` grammar so `,`, `(`, `)`,
|
|
37
37
|
* `.`, `"` and `\` in the search stay literal instead of being read as more filters. Inside the
|
|
38
|
-
* quotes only `"` and `\` need escaping, which is what PostgREST defines
|
|
38
|
+
* quotes only `"` and `\` need escaping, which is what PostgREST defines - the `\,` the list
|
|
39
39
|
* endpoints used to emit was never an escape at all. LIKE's own wildcards `%` and `_` are left
|
|
40
40
|
* alone and still match as wildcards; they only widen the caller's own search, never its scope.
|
|
41
41
|
*/
|
|
42
42
|
const ilikeContains = (value) => `"%${value.replace(/[\\"]/g, '\\$&')}%"`;
|
|
43
43
|
exports.ilikeContains = ilikeContains;
|
|
44
|
-
/** The `or=(...)` argument matching `search` against any of `columns`
|
|
44
|
+
/** The `or=(...)` argument matching `search` against any of `columns` - `name.ilike."%x%",…`. */
|
|
45
45
|
const orIlike = (columns, search) => columns.map((column) => `${column}.ilike.${(0, exports.ilikeContains)(search)}`).join(',');
|
|
46
46
|
exports.orIlike = orIlike;
|
|
47
47
|
/**
|
|
48
48
|
* `date` columns take an ISO string. The shared `CreateProjectDto`/`UpdateProjectDto` types
|
|
49
|
-
* declare `Date`, but over the wire the value is the validated `@IsDateString` string
|
|
50
|
-
* must reach Postgres byte-identical ('2026-04-22' is a plain date, not an instant)
|
|
49
|
+
* declare `Date`, but over the wire the value is the validated `@IsDateString` string - which
|
|
50
|
+
* must reach Postgres byte-identical ('2026-04-22' is a plain date, not an instant) - so only a
|
|
51
51
|
* real `Date` (a portal caller) is serialized.
|
|
52
52
|
*/
|
|
53
53
|
const toDateInput = (value) => value instanceof Date ? value.toISOString() : value;
|
|
@@ -66,7 +66,7 @@ exports.USER_PROFILE_EMBED = `(
|
|
|
66
66
|
)`;
|
|
67
67
|
/**
|
|
68
68
|
* The `MmsList` envelope every paginated read returns. `rows` is `unknown` because each select
|
|
69
|
-
* string produces its own row shape
|
|
69
|
+
* string produces its own row shape - the caller's `T` is what declares it. `count` is
|
|
70
70
|
* PostgREST's estimate, so an empty page reports 0 total pages rather than 1.
|
|
71
71
|
*/
|
|
72
72
|
const toMmsList = (rows, page, pageSize, count) => {
|
|
@@ -85,7 +85,7 @@ const toMmsList = (rows, page, pageSize, count) => {
|
|
|
85
85
|
exports.toMmsList = toMmsList;
|
|
86
86
|
// The soft archive/delete patches projects, tasks and clients all write: the flag the transition
|
|
87
87
|
// sets, plus the who/when stamps named after it. `unarchived`/`restored` clear the flag their
|
|
88
|
-
// counterpart set and stamp their own columns
|
|
88
|
+
// counterpart set and stamp their own columns - the earlier ones stay as the history.
|
|
89
89
|
const archivedPatch = (by) => ({
|
|
90
90
|
archived: true,
|
|
91
91
|
archived_at: new Date().toISOString(),
|
package/dist/users/dto.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare class AddUserDto implements CreateUserDtoType {
|
|
|
16
16
|
* Optional: an internally created user - the owner at signup - has no client
|
|
17
17
|
* request behind it, and one is generated for it.
|
|
18
18
|
*
|
|
19
|
-
* Still v4: the key is not an `mms_id`
|
|
19
|
+
* Still v4: the key is not an `mms_id` - the column is `text` - and the api fills a missing
|
|
20
20
|
* one with `randomUUID()`, which is v4.
|
|
21
21
|
*/
|
|
22
22
|
readonly idempotency_key?: string;
|
|
@@ -50,13 +50,13 @@ export declare class UpdateUserDto implements UpdateUserDtoType {
|
|
|
50
50
|
readonly emergency_contact_email?: string | null;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* `PATCH /users/me`
|
|
53
|
+
* `PATCH /users/me` - what a member may change about themselves.
|
|
54
54
|
*
|
|
55
55
|
* Every field here also appears on UpdateUserDto with the same constraints; what makes
|
|
56
56
|
* this a separate class is what it *omits*. The api's ValidationPipe runs
|
|
57
57
|
* `whitelist + forbidNonWhitelisted`, so a self-service request carrying `job_title`,
|
|
58
58
|
* `manager_id`, `employment_type`, `start_date`, `end_date` or `profile_image` is
|
|
59
|
-
* rejected with a 400 rather than silently ignored
|
|
59
|
+
* rejected with a 400 rather than silently ignored - the narrowing is the guard.
|
|
60
60
|
*/
|
|
61
61
|
export declare class UpdateMyProfileDto implements UpdateMyProfileDtoType {
|
|
62
62
|
readonly name: string;
|
package/dist/users/dto.js
CHANGED
|
@@ -38,7 +38,7 @@ class AddUserDto {
|
|
|
38
38
|
* Optional: an internally created user - the owner at signup - has no client
|
|
39
39
|
* request behind it, and one is generated for it.
|
|
40
40
|
*
|
|
41
|
-
* Still v4: the key is not an `mms_id`
|
|
41
|
+
* Still v4: the key is not an `mms_id` - the column is `text` - and the api fills a missing
|
|
42
42
|
* one with `randomUUID()`, which is v4.
|
|
43
43
|
*/
|
|
44
44
|
idempotency_key;
|
|
@@ -87,7 +87,7 @@ __decorate([
|
|
|
87
87
|
__metadata("design:type", String)
|
|
88
88
|
], AddUserDto.prototype, "idempotency_key", void 0);
|
|
89
89
|
// `active_in` arrives as a CSV string and is transformed to booleans, so it is omitted from the
|
|
90
|
-
// implemented contract
|
|
90
|
+
// implemented contract - the same exception GetClientsDto makes for status_id_in.
|
|
91
91
|
class GetUsersQueryDto {
|
|
92
92
|
search;
|
|
93
93
|
page = 1;
|
|
@@ -284,13 +284,13 @@ __decorate([
|
|
|
284
284
|
__metadata("design:type", Object)
|
|
285
285
|
], UpdateUserDto.prototype, "emergency_contact_email", void 0);
|
|
286
286
|
/**
|
|
287
|
-
* `PATCH /users/me`
|
|
287
|
+
* `PATCH /users/me` - what a member may change about themselves.
|
|
288
288
|
*
|
|
289
289
|
* Every field here also appears on UpdateUserDto with the same constraints; what makes
|
|
290
290
|
* this a separate class is what it *omits*. The api's ValidationPipe runs
|
|
291
291
|
* `whitelist + forbidNonWhitelisted`, so a self-service request carrying `job_title`,
|
|
292
292
|
* `manager_id`, `employment_type`, `start_date`, `end_date` or `profile_image` is
|
|
293
|
-
* rejected with a 400 rather than silently ignored
|
|
293
|
+
* rejected with a 400 rather than silently ignored - the narrowing is the guard.
|
|
294
294
|
*/
|
|
295
295
|
class UpdateMyProfileDto {
|
|
296
296
|
name;
|