@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
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ npm install @managemint-solutions/sdk @supabase/supabase-js
|
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
`@supabase/supabase-js` (^2.103.0), `class-validator` (^0.15) and `class-transformer` (^0.5)
|
|
13
|
-
are peer dependencies
|
|
13
|
+
are peer dependencies - the request DTOs (`AddStatusDto`, `UpdateStatusDto`, `StatusIdDto`,
|
|
14
14
|
`GetStatusesDto`, `AddAdditionalDetailsDto`, `UpdateAdditionalDetailsDto`,
|
|
15
15
|
`AdditionalDetailsIdDto`, `GetAdditionalDetailsDto`, `AddNoteDto`, `NoteIdDto`,
|
|
16
16
|
`GetNotesDto`, `SupportingFileIdDto`, `GetSupportingFilesDto`, `GetClientsDto`, `ClientIdDto`,
|
|
@@ -39,7 +39,7 @@ const supabase = createSupabaseClient({
|
|
|
39
39
|
|
|
40
40
|
// Inputs and outputs are the shared types from @managemint-solutions/entities.
|
|
41
41
|
const statuses: StatusEntity[] = await supabase.statuses.list(StatusEntityTypes.PROJECT);
|
|
42
|
-
// `is_complete` marks the finished state and is optional
|
|
42
|
+
// `is_complete` marks the finished state and is optional - it defaults to false on create and
|
|
43
43
|
// is left unchanged on update when omitted. `is_fail` and `is_triage` are read back on every
|
|
44
44
|
// status but are not settable through the SDK yet.
|
|
45
45
|
const created = await supabase.statuses.create({
|
|
@@ -100,7 +100,7 @@ import { SupportingFilesEntityTypes } from '@managemint-solutions/entities/suppo
|
|
|
100
100
|
// Supporting files hang off another entity, and cover Supabase Storage as well as the table.
|
|
101
101
|
const files = await supabase.supportingFiles.list(SupportingFilesEntityTypes.PROJECT, projectId);
|
|
102
102
|
await supabase.supportingFiles.upload(SupportingFilesEntityTypes.PROJECT, projectId, {
|
|
103
|
-
files: uploaded, // web `File`s
|
|
103
|
+
files: uploaded, // web `File`s - uploaded to the `supporting-files` bucket, then rowed up
|
|
104
104
|
});
|
|
105
105
|
const { url, ttl } = await supabase.supportingFiles.getPresignedUrl(files[0].mms_id); // 5 min
|
|
106
106
|
await supabase.supportingFiles.remove(files[0].mms_id); // deletes the row and the object
|
|
@@ -128,7 +128,7 @@ await supabase.clients.getById(client.mms_id);
|
|
|
128
128
|
await supabase.clients.update(client.mms_id, { ...client, name: 'Acme Ltd' });
|
|
129
129
|
await supabase.clients.archive(client.mms_id); // archived + archived_at/by
|
|
130
130
|
await supabase.clients.unarchive(client.mms_id); // archived = false + unarchived_at/by
|
|
131
|
-
await supabase.clients.remove(client.mms_id); // soft delete
|
|
131
|
+
await supabase.clients.remove(client.mms_id); // soft delete - deleted + deleted_at/by
|
|
132
132
|
await supabase.clients.restore(client.mms_id); // deleted = false + restored_at/by
|
|
133
133
|
```
|
|
134
134
|
|
|
@@ -145,7 +145,7 @@ const page = await supabase.projects.list({
|
|
|
145
145
|
client_id: clientId, // a UUID, or the base64-JSON option value a select sends
|
|
146
146
|
});
|
|
147
147
|
// `search` matches name/specification; `created_*`, `start_date_*` and `end_date_*` bound the
|
|
148
|
-
// date columns, and `status_id_in` filters by status. `status_id_not_in` is the exclusion form
|
|
148
|
+
// date columns, and `status_id_in` filters by status. `status_id_not_in` is the exclusion form -
|
|
149
149
|
// it keeps rows with no status, which `status_id_in` cannot express. `completed` is carried but
|
|
150
150
|
// not acted on here: the api resolves it to status ids against the statuses catalogue first.
|
|
151
151
|
|
|
@@ -182,7 +182,7 @@ const page = await supabase.tasks.list({
|
|
|
182
182
|
});
|
|
183
183
|
// `search` matches name/description, and `client_id`, `assigned_to_id`, the date ranges and
|
|
184
184
|
// `status_id_in` narrow it further. `status_id_not_in` excludes statuses while keeping rows with
|
|
185
|
-
// no status; `completed` is carried but not acted on here
|
|
185
|
+
// no status; `completed` is carried but not acted on here - the api resolves it to status ids
|
|
186
186
|
// against the statuses catalogue first.
|
|
187
187
|
|
|
188
188
|
const task = await supabase.tasks.create({
|
|
@@ -209,11 +209,11 @@ task carries its embedded `status`, `assigned_to`, `project` and `client`.
|
|
|
209
209
|
|
|
210
210
|
The client decodes `{ mms_id, organization_id }` from the JWT itself and scopes every query
|
|
211
211
|
by `organization_id`. `supabase.supabase` is the raw `@supabase/supabase-js` client
|
|
212
|
-
(`TypedSupabaseClient`)
|
|
212
|
+
(`TypedSupabaseClient`) - the escape hatch for queries not yet covered by a resource.
|
|
213
213
|
|
|
214
214
|
## Audit
|
|
215
215
|
|
|
216
|
-
Every write a resource makes is logged to `audit_logs` by the SDK itself
|
|
216
|
+
Every write a resource makes is logged to `audit_logs` by the SDK itself - there are no database
|
|
217
217
|
triggers and no audit code in any feature. A write records one row carrying the action
|
|
218
218
|
(`INSERT` / `UPDATE` / `ARCHIVE` / `UNARCHIVE` / `DELETE` / `RESTORE`), the entity type and id, who
|
|
219
219
|
did it, and a `changes` diff of the flat table row as real JSON (`{ field: { old, new } }`).
|
|
@@ -245,7 +245,7 @@ const supabase = createSupabaseClient({
|
|
|
245
245
|
```
|
|
246
246
|
|
|
247
247
|
With the Nest decorator there is nowhere to pass config per request, so the handler is registered
|
|
248
|
-
once at startup
|
|
248
|
+
once at startup - after Sentry is initialised, before the app listens:
|
|
249
249
|
|
|
250
250
|
```ts
|
|
251
251
|
import { configureSupabaseUserClient } from '@managemint-solutions/sdk/nest';
|
|
@@ -254,8 +254,8 @@ configureSupabaseUserClient({ onAuditError: (error) => Sentry.captureException(e
|
|
|
254
254
|
```
|
|
255
255
|
|
|
256
256
|
Service-role work with no caller (webhooks, the queue worker, provisioning) goes through the
|
|
257
|
-
service client's organization scope, which supplies the audit actor
|
|
258
|
-
work
|
|
257
|
+
service client's organization scope, which supplies the audit actor - `mms_id: null` for system
|
|
258
|
+
work - and hands out audited tables for anything the SDK has no resource for:
|
|
259
259
|
|
|
260
260
|
```ts
|
|
261
261
|
import { createSupabaseServiceClient } from '@managemint-solutions/sdk';
|
|
@@ -274,20 +274,20 @@ await users.transition(userId, AuditTrailAction.DELETE, { deleted: true }); // r
|
|
|
274
274
|
const deleted = await users.remove(userId); // hard delete; returns the old row
|
|
275
275
|
```
|
|
276
276
|
|
|
277
|
-
Cross-tenant lookups
|
|
278
|
-
organization with a Paystack customer
|
|
277
|
+
Cross-tenant lookups - the organization behind a Paystack code, a ledger row by id, every
|
|
278
|
+
organization with a Paystack customer - hang off `service` itself, because a webhook or cron does
|
|
279
279
|
not know its tenant until it looks one up.
|
|
280
280
|
|
|
281
281
|
`update` / `transition` / `remove` pre-read the flat row (by `mms_id`, and by `organization_id`
|
|
282
|
-
unless `scopeByOrganization: false`
|
|
282
|
+
unless `scopeByOrganization: false` - the `organizations` table is its own tenant), so a missing
|
|
283
283
|
row surfaces as the usual 406. The write itself still throws `SupabaseClientError` on failure;
|
|
284
284
|
only the audit insert is swallowed.
|
|
285
285
|
|
|
286
|
-
Only what goes through the SDK/API is logged
|
|
286
|
+
Only what goes through the SDK/API is logged - a raw PostgREST write or a dashboard edit is not.
|
|
287
287
|
|
|
288
288
|
## Auth
|
|
289
289
|
|
|
290
|
-
`SupabaseClient` needs the caller's JWT, which the auth flows are there to produce
|
|
290
|
+
`SupabaseClient` needs the caller's JWT, which the auth flows are there to produce - so sign-in,
|
|
291
291
|
refresh and the password-reset exchanges live on a separate `SupabaseAuthClient`, built from the
|
|
292
292
|
publishable key alone.
|
|
293
293
|
|
|
@@ -296,7 +296,7 @@ import { createSupabaseAuthClient } from '@managemint-solutions/sdk';
|
|
|
296
296
|
|
|
297
297
|
const auth = createSupabaseAuthClient({
|
|
298
298
|
url: process.env.SUPABASE_URL!,
|
|
299
|
-
key: process.env.SUPABASE_PUBLISHABLE_KEY!, // no caller token
|
|
299
|
+
key: process.env.SUPABASE_PUBLISHABLE_KEY!, // no caller token - these callers are logged out
|
|
300
300
|
});
|
|
301
301
|
|
|
302
302
|
const { access_token, refresh_token } = await auth.signIn({ email, password });
|
|
@@ -314,7 +314,7 @@ await auth.resetPassword({ password, access_token, refresh_token });
|
|
|
314
314
|
client carrying the fresh access token; a failed stamp never fails the login. `signOut` takes the
|
|
315
315
|
caller's access token because this client holds no session of its own: GoTrue's logout endpoint
|
|
316
316
|
authenticates with the token itself, and the `local` scope ends that device's session only. Build
|
|
317
|
-
one of these per request
|
|
317
|
+
one of these per request - supabase-js keeps the session `signIn`, `verifyOtp` and `setSession`
|
|
318
318
|
produce in the client's memory, so a shared instance would leak sessions between callers.
|
|
319
319
|
`auth.supabase` is the raw `@supabase/supabase-js` client.
|
|
320
320
|
|
|
@@ -361,7 +361,7 @@ and reads rows only.
|
|
|
361
361
|
## Pricebooks
|
|
362
362
|
|
|
363
363
|
The price catalog (`pricebooks`, `pricebook_modules`, `billing_modules`) is readable by `anon`
|
|
364
|
-
by design
|
|
364
|
+
by design - an unauthenticated signup prices against it - so `pricebooks` hangs off the same
|
|
365
365
|
bearer-less `SupabaseAuthClient` as the auth flows, takes no auth context and scopes by nothing.
|
|
366
366
|
|
|
367
367
|
```ts
|
|
@@ -381,7 +381,7 @@ row joined with its `billing_modules` catalog entry). Exactly one pricebook is a
|
|
|
381
381
|
|
|
382
382
|
## Configs
|
|
383
383
|
|
|
384
|
-
`public.configs` holds global named JSON configs
|
|
384
|
+
`public.configs` holds global named JSON configs - one `value` object per row, with no
|
|
385
385
|
organization on it. Every signed-in caller may read, and only the service role may write, so
|
|
386
386
|
`configs` hangs off the caller's client and the service client alike and a write on the former
|
|
387
387
|
fails at the database (Scott edits the rows in Supabase Studio for now). A feature-flag set is
|
|
@@ -405,9 +405,9 @@ await service.configs.remove('maintenance'); // hard delete, service role only
|
|
|
405
405
|
The first slice of the HR module: `leave_types`, `leave_entitlements` and `leave_requests`, one
|
|
406
406
|
org-scoped audited resource each on the caller's client. Every date is a calendar date
|
|
407
407
|
(`YYYY-MM-DD`, the columns are `date`), and the DTOs refuse anything else; body fields are
|
|
408
|
-
required keys, with `null` for an empty one. Balances are never stored
|
|
408
|
+
required keys, with `null` for an empty one. Balances are never stored - an entitlement's
|
|
409
409
|
`days_used` / `days_pending` / `days_remaining` are computed on read from the approved and pending
|
|
410
|
-
requests of that user and type whose start date falls in its period
|
|
410
|
+
requests of that user and type whose start date falls in its period - and a request's `days` is
|
|
411
411
|
a snapshot taken when it was filed, so a holiday added later does not rewrite approved history.
|
|
412
412
|
|
|
413
413
|
The calendar arithmetic is plain exported functions, shared with the api:
|
|
@@ -452,7 +452,7 @@ accepts `leave_requests` and `leave_entitlements` alongside the CRM entity types
|
|
|
452
452
|
|
|
453
453
|
The public-holiday calendar, `public_holidays`, on the caller's client as `supabase.publicHolidays`.
|
|
454
454
|
Global like configs: there is no organization on a row, every signed-in caller reads the same
|
|
455
|
-
list, and only the service role may write
|
|
455
|
+
list, and only the service role may write - Scott maintains it in Supabase Studio, so the SDK is
|
|
456
456
|
read-only here. Several holidays may share a date; only the ones with `religion` null count as a
|
|
457
457
|
day off for everyone.
|
|
458
458
|
|
|
@@ -467,7 +467,7 @@ Every error the SDK throws is a `SupabaseClientError` carrying the HTTP `status`
|
|
|
467
467
|
with and the client-facing body, `toResponse()` → `{ error, message }`. Postgrest/Postgres
|
|
468
468
|
errors are mapped by code: `PGRST*` codes to their documented statuses, an RLS or grant denial
|
|
469
469
|
(`42501`) to **403**, a unique or foreign-key violation (`23505`, `23503`) to **409**, a not-null,
|
|
470
|
-
check or malformed-literal failure (`23502`, `23514`, `22P02`) to **400**, anything else 500
|
|
470
|
+
check or malformed-literal failure (`23502`, `23514`, `22P02`) to **400**, anything else 500 -
|
|
471
471
|
the message is always Postgrest's own. Storage errors carry the storage error's own HTTP status
|
|
472
472
|
(500 when it has none); Supabase Auth errors likewise, as `Authentication Error`; a rejected or
|
|
473
473
|
missing JWT is `401 Authentication Error`. Consumers return these as-is instead of mapping
|
|
@@ -505,7 +505,7 @@ once a day however busy it is. Every other call is one round trip. Values are JS
|
|
|
505
505
|
by the client; `get<T>` / `getField<T>` are the caller's promise about what was written, not a
|
|
506
506
|
check.
|
|
507
507
|
|
|
508
|
-
The client takes its URL and token as config
|
|
508
|
+
The client takes its URL and token as config - the SDK reads no environment variables - and
|
|
509
509
|
retries once with a three-second timeout per request. It **throws** on a failed call: what a
|
|
510
510
|
failure means is the caller's decision, and both callers today fail open (a miss on the portal,
|
|
511
511
|
a Sentry report on the api). The key prefix and TTL for the profile hash live in
|
|
@@ -533,7 +533,7 @@ malformed token becomes an `UnauthorizedException`. The DTOs work with Nest's gl
|
|
|
533
533
|
`ValidationPipe` as-is (`whitelist`, `forbidNonWhitelisted`, `transform`).
|
|
534
534
|
|
|
535
535
|
`toHttpException(error)` converts a `SupabaseClientError` into an `HttpException` with the
|
|
536
|
-
same status and body
|
|
536
|
+
same status and body - call it from your global exception filter so services can let SDK
|
|
537
537
|
errors propagate without any try/catch.
|
|
538
538
|
|
|
539
539
|
## Development
|
|
@@ -541,7 +541,7 @@ errors propagate without any try/catch.
|
|
|
541
541
|
```bash
|
|
542
542
|
npm install
|
|
543
543
|
npm test # jest + ts-jest; specs live in src/<resource>/tests/*.spec.ts
|
|
544
|
-
npm run build # tsc -p tsconfig.build.json (excludes specs), CommonJS output in dist/
|
|
544
|
+
npm run build # tsc -p tsconfig.build.json (excludes specs), CommonJS output in dist/ - the type gate
|
|
545
545
|
```
|
|
546
546
|
|
|
547
547
|
The SDK carries no generated database types: the schema of record is the api's
|
package/dist/audit/dto.js
CHANGED
|
@@ -67,7 +67,7 @@ __decorate([
|
|
|
67
67
|
(0, class_validator_1.IsOptional)(),
|
|
68
68
|
(0, validators_1.IsNullable)()
|
|
69
69
|
// Still v4: `audit_logs.created_by` is a `users.mms_id`, which is the GoTrue auth uid the
|
|
70
|
-
// signup trigger copies out of `auth.users.id`
|
|
70
|
+
// signup trigger copies out of `auth.users.id` - the table's uuidv7 default never fires.
|
|
71
71
|
,
|
|
72
72
|
(0, class_validator_1.IsUUID)('4', { message: 'Created by must be a valid UUID v4' }),
|
|
73
73
|
__metadata("design:type", Object)
|
package/dist/audit/index.d.ts
CHANGED
|
@@ -6,17 +6,17 @@ import type { GetAuditTrailDto } from './dto';
|
|
|
6
6
|
export * from './dto';
|
|
7
7
|
/**
|
|
8
8
|
* Who a write is attributed to. `AuthContext` is assignable to it; the api's admin-client
|
|
9
|
-
* modules build one by hand (system jobs pass `mms_id: null`
|
|
9
|
+
* modules build one by hand (system jobs pass `mms_id: null` - `created_by` is nullable).
|
|
10
10
|
*/
|
|
11
11
|
export type AuditActor = {
|
|
12
12
|
mms_id: string | null;
|
|
13
13
|
organization_id: string;
|
|
14
14
|
};
|
|
15
|
-
/** A flat database row
|
|
15
|
+
/** A flat database row - always `select('*')`, never an entity shape with embedded joins. */
|
|
16
16
|
export type AuditRow = Record<string, unknown>;
|
|
17
17
|
/**
|
|
18
18
|
* Drops the internal `id bigint` from a row on its way out of the SDK. Every table has one
|
|
19
|
-
* alongside its public `mms_id uuid`, and only the database is meant to see it
|
|
19
|
+
* alongside its public `mms_id uuid`, and only the database is meant to see it - a caller that
|
|
20
20
|
* never learns the sequence cannot come to depend on it, or leak how many rows there are.
|
|
21
21
|
*/
|
|
22
22
|
export declare const withoutInternalId: <T extends Record<string, unknown>>(row: T) => Omit<T, "id">;
|
|
@@ -42,12 +42,12 @@ export type AuditErrorHandler = (error: unknown, entries: AuditEntry[]) => void;
|
|
|
42
42
|
export declare const AUDIT_EXCLUDED_COLUMNS: ReadonlySet<string>;
|
|
43
43
|
/**
|
|
44
44
|
* Columns whose *value* never reaches the trail. `audit_logs` is readable by any member with
|
|
45
|
-
* `read_logs`, so a value copied into `changes` is a value published to the whole organization
|
|
45
|
+
* `read_logs`, so a value copied into `changes` is a value published to the whole organization -
|
|
46
46
|
* and these are either provider credentials and identifiers (the `paystack_*` customer and
|
|
47
47
|
* subscription codes, card metadata, tokens, an idempotency key that would let a replay be
|
|
48
48
|
* forged) or content the base table deliberately withholds (`authenticated` has no grant on
|
|
49
49
|
* `notifications.rendered_content`, so the rendered email must not arrive by the back door).
|
|
50
|
-
* The column still shows up in the trail
|
|
50
|
+
* The column still shows up in the trail - that it changed is the point - with both sides
|
|
51
51
|
* replaced by `'[redacted]'`.
|
|
52
52
|
*/
|
|
53
53
|
export declare const AUDIT_REDACTED_COLUMNS: ReadonlySet<string>;
|
|
@@ -57,18 +57,18 @@ export declare const isRedactedColumn: (name: string) => boolean;
|
|
|
57
57
|
* The fields that differ between two flat rows, as `{ field: { old, new } }`. Values stay real
|
|
58
58
|
* JSON (booleans stay booleans, jsonb stays an object) and are compared by serialized value, so
|
|
59
59
|
* arrays and objects are equal when their contents are. A redacted column is compared on its
|
|
60
|
-
* real values and only then redacted, so it is reported when
|
|
60
|
+
* real values and only then redacted, so it is reported when - and only when - it truly moved.
|
|
61
61
|
*/
|
|
62
62
|
export declare const diffChanges: (before: AuditRow, after: AuditRow) => AuditChanges;
|
|
63
63
|
/**
|
|
64
|
-
* A whole row as a change set
|
|
64
|
+
* A whole row as a change set - `'new'` for an insert, `'old'` for a hard delete - so the trail
|
|
65
65
|
* stays self-contained once the row is gone. Columns that hold nothing are left out; a redacted
|
|
66
66
|
* column that holds something is present, as `'[redacted]'`.
|
|
67
67
|
*/
|
|
68
68
|
export declare const snapshotChanges: (row: AuditRow, side: "new" | "old") => AuditChanges;
|
|
69
69
|
/**
|
|
70
70
|
* The `audit_logs` table. Reads are the audit trail endpoint; writes go through `record`, which
|
|
71
|
-
* every audited write calls for itself
|
|
71
|
+
* every audited write calls for itself - no feature ever writes an audit line by hand.
|
|
72
72
|
*/
|
|
73
73
|
export declare class AuditResource {
|
|
74
74
|
private readonly supabase;
|
|
@@ -76,7 +76,7 @@ export declare class AuditResource {
|
|
|
76
76
|
private readonly onError?;
|
|
77
77
|
constructor(supabase: SupabaseJsClient, actor: AuditActor, onError?: AuditErrorHandler | undefined);
|
|
78
78
|
/**
|
|
79
|
-
* Writes the audit row(s)
|
|
79
|
+
* Writes the audit row(s) - one insert, however many entries. **Never throws**: a logging
|
|
80
80
|
* problem must not fail the business write, so a failure goes to `onAuditError`
|
|
81
81
|
* (default `console.error`) instead.
|
|
82
82
|
*/
|
|
@@ -97,7 +97,7 @@ type AuditedPayload = AuditRow | ((before: AuditRow) => AuditRow);
|
|
|
97
97
|
/** The keyed update every audited write builds, before any caller-supplied guard. */
|
|
98
98
|
declare const buildUpdate: (supabase: SupabaseJsClient, table: string, id: string, payload: AuditRow, organizationId: string | null) => import("@supabase/postgrest-js").PostgrestFilterBuilder<any, any, any, null, string, unknown, "PATCH", false>;
|
|
99
99
|
/**
|
|
100
|
-
* Narrows an audited update to the rows it may still touch
|
|
100
|
+
* Narrows an audited update to the rows it may still touch - the compare-and-set half of a
|
|
101
101
|
* guarded write, e.g. `(query) => query.eq('billing_status', expectedFrom)`.
|
|
102
102
|
*/
|
|
103
103
|
export type AuditGuard = (builder: ReturnType<typeof buildUpdate>) => ReturnType<typeof buildUpdate>;
|
|
@@ -106,10 +106,10 @@ export type AuditedTable = {
|
|
|
106
106
|
insertMany(payloads: AuditRow[]): Promise<AuditRow[]>;
|
|
107
107
|
update(id: string, payload: AuditedPayload): Promise<AuditRow>;
|
|
108
108
|
/**
|
|
109
|
-
* An update that only lands while `guard` still holds
|
|
109
|
+
* An update that only lands while `guard` still holds - the filter *is* the concurrency
|
|
110
110
|
* control, not just a key. Resolves to `null` when the guard matched nothing (someone else
|
|
111
111
|
* moved the row first): nothing was written, so nothing is audited. Use it wherever losing
|
|
112
|
-
* the race must be a no-op rather than an overwrite
|
|
112
|
+
* the race must be a no-op rather than an overwrite - billing status moves, the
|
|
113
113
|
* send-exactly-once gates, first-occurrence-only delivery rollups.
|
|
114
114
|
*/
|
|
115
115
|
updateWhere(id: string, guard: AuditGuard, payload: AuditedPayload): Promise<AuditRow | null>;
|
package/dist/audit/index.js
CHANGED
|
@@ -21,7 +21,7 @@ const transforms_1 = require("../transforms");
|
|
|
21
21
|
__exportStar(require("./dto"), exports);
|
|
22
22
|
/**
|
|
23
23
|
* Drops the internal `id bigint` from a row on its way out of the SDK. Every table has one
|
|
24
|
-
* alongside its public `mms_id uuid`, and only the database is meant to see it
|
|
24
|
+
* alongside its public `mms_id uuid`, and only the database is meant to see it - a caller that
|
|
25
25
|
* never learns the sequence cannot come to depend on it, or leak how many rows there are.
|
|
26
26
|
*/
|
|
27
27
|
const withoutInternalId = (row) => {
|
|
@@ -56,12 +56,12 @@ exports.AUDIT_EXCLUDED_COLUMNS = new Set([
|
|
|
56
56
|
]);
|
|
57
57
|
/**
|
|
58
58
|
* Columns whose *value* never reaches the trail. `audit_logs` is readable by any member with
|
|
59
|
-
* `read_logs`, so a value copied into `changes` is a value published to the whole organization
|
|
59
|
+
* `read_logs`, so a value copied into `changes` is a value published to the whole organization -
|
|
60
60
|
* and these are either provider credentials and identifiers (the `paystack_*` customer and
|
|
61
61
|
* subscription codes, card metadata, tokens, an idempotency key that would let a replay be
|
|
62
62
|
* forged) or content the base table deliberately withholds (`authenticated` has no grant on
|
|
63
63
|
* `notifications.rendered_content`, so the rendered email must not arrive by the back door).
|
|
64
|
-
* The column still shows up in the trail
|
|
64
|
+
* The column still shows up in the trail - that it changed is the point - with both sides
|
|
65
65
|
* replaced by `'[redacted]'`.
|
|
66
66
|
*/
|
|
67
67
|
exports.AUDIT_REDACTED_COLUMNS = new Set([
|
|
@@ -87,7 +87,7 @@ const normalize = (value) => value ?? null;
|
|
|
87
87
|
* The fields that differ between two flat rows, as `{ field: { old, new } }`. Values stay real
|
|
88
88
|
* JSON (booleans stay booleans, jsonb stays an object) and are compared by serialized value, so
|
|
89
89
|
* arrays and objects are equal when their contents are. A redacted column is compared on its
|
|
90
|
-
* real values and only then redacted, so it is reported when
|
|
90
|
+
* real values and only then redacted, so it is reported when - and only when - it truly moved.
|
|
91
91
|
*/
|
|
92
92
|
const diffChanges = (before, after) => {
|
|
93
93
|
const changes = {};
|
|
@@ -106,7 +106,7 @@ const diffChanges = (before, after) => {
|
|
|
106
106
|
};
|
|
107
107
|
exports.diffChanges = diffChanges;
|
|
108
108
|
/**
|
|
109
|
-
* A whole row as a change set
|
|
109
|
+
* A whole row as a change set - `'new'` for an insert, `'old'` for a hard delete - so the trail
|
|
110
110
|
* stays self-contained once the row is gone. Columns that hold nothing are left out; a redacted
|
|
111
111
|
* column that holds something is present, as `'[redacted]'`.
|
|
112
112
|
*/
|
|
@@ -134,7 +134,7 @@ const AUDIT_TRAIL_SELECT = `
|
|
|
134
134
|
`;
|
|
135
135
|
/**
|
|
136
136
|
* The `audit_logs` table. Reads are the audit trail endpoint; writes go through `record`, which
|
|
137
|
-
* every audited write calls for itself
|
|
137
|
+
* every audited write calls for itself - no feature ever writes an audit line by hand.
|
|
138
138
|
*/
|
|
139
139
|
class AuditResource {
|
|
140
140
|
supabase;
|
|
@@ -146,7 +146,7 @@ class AuditResource {
|
|
|
146
146
|
this.onError = onError;
|
|
147
147
|
}
|
|
148
148
|
/**
|
|
149
|
-
* Writes the audit row(s)
|
|
149
|
+
* Writes the audit row(s) - one insert, however many entries. **Never throws**: a logging
|
|
150
150
|
* problem must not fail the business write, so a failure goes to `onAuditError`
|
|
151
151
|
* (default `console.error`) instead.
|
|
152
152
|
*/
|
|
@@ -257,7 +257,7 @@ const auditedTable = (supabase, actor, audit, options) => {
|
|
|
257
257
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
258
258
|
const after = data;
|
|
259
259
|
await recordWrite(id, action, before, after);
|
|
260
|
-
// `before` stays whole
|
|
260
|
+
// `before` stays whole - it is internal to the diff - but what the caller gets does not
|
|
261
261
|
// carry the internal `id`.
|
|
262
262
|
return (0, exports.withoutInternalId)(after);
|
|
263
263
|
};
|
|
@@ -269,7 +269,7 @@ const auditedTable = (supabase, actor, audit, options) => {
|
|
|
269
269
|
.maybeSingle();
|
|
270
270
|
if (error)
|
|
271
271
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
272
|
-
// The guard lost the race
|
|
272
|
+
// The guard lost the race - the row is untouched, so there is nothing to audit.
|
|
273
273
|
if (!data)
|
|
274
274
|
return null;
|
|
275
275
|
const after = data;
|
|
@@ -27,13 +27,13 @@ export declare class SupabaseAuthClient {
|
|
|
27
27
|
* other sessions alone.
|
|
28
28
|
*/
|
|
29
29
|
signOut(accessToken: string): Promise<void>;
|
|
30
|
-
/** `redirectTo` is passed in
|
|
30
|
+
/** `redirectTo` is passed in - the SDK reads no environment variables. */
|
|
31
31
|
sendPasswordResetEmail(input: SendPasswordResetEmailDto, redirectTo: string): Promise<void>;
|
|
32
32
|
exchangeRecoveryToken(input: ExchangeTokenHashDto): Promise<ExchangeTokenResponse>;
|
|
33
33
|
exchangeConfirmToken(input: ExchangeTokenHashDto): Promise<ExchangeTokenResponse>;
|
|
34
34
|
resetPassword(input: ResetPasswordDto): Promise<void>;
|
|
35
35
|
/**
|
|
36
|
-
* Stamps `users.last_logged_in` as the user who just signed in
|
|
36
|
+
* Stamps `users.last_logged_in` as the user who just signed in - the users UPDATE policy
|
|
37
37
|
* allows `auth.uid() = mms_id`, so the write goes through a throwaway client carrying the
|
|
38
38
|
* fresh access token rather than this session-less one. Parity with the api: the result is
|
|
39
39
|
* ignored, a failed stamp must not fail the login.
|
|
@@ -74,7 +74,7 @@ class SupabaseAuthClient {
|
|
|
74
74
|
if (error)
|
|
75
75
|
throw (0, errors_1.mapAuthError)(error);
|
|
76
76
|
}
|
|
77
|
-
/** `redirectTo` is passed in
|
|
77
|
+
/** `redirectTo` is passed in - the SDK reads no environment variables. */
|
|
78
78
|
async sendPasswordResetEmail(input, redirectTo) {
|
|
79
79
|
const { error } = await this.supabase.auth.resetPasswordForEmail(input.email, { redirectTo });
|
|
80
80
|
if (error)
|
|
@@ -116,7 +116,7 @@ class SupabaseAuthClient {
|
|
|
116
116
|
throw (0, errors_1.mapAuthError)(updateError);
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
119
|
-
* Stamps `users.last_logged_in` as the user who just signed in
|
|
119
|
+
* Stamps `users.last_logged_in` as the user who just signed in - the users UPDATE policy
|
|
120
120
|
* allows `auth.uid() = mms_id`, so the write goes through a throwaway client carrying the
|
|
121
121
|
* fresh access token rather than this session-less one. Parity with the api: the result is
|
|
122
122
|
* ignored, a failed stamp must not fail the login.
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type SupabaseClient as SupabaseJsClient } from '@supabase/supabase-js';
|
|
2
2
|
/**
|
|
3
|
-
* A raw supabase-js client, built the one way the SDK ever needs one: server-side and stateless
|
|
4
|
-
* nothing is stored, refreshed or read out of a URL
|
|
3
|
+
* A raw supabase-js client, built the one way the SDK ever needs one: server-side and stateless -
|
|
4
|
+
* nothing is stored, refreshed or read out of a URL - carrying the caller's bearer token when
|
|
5
5
|
* there is one, so RLS sees them.
|
|
6
6
|
*/
|
|
7
7
|
export declare const createRawClient: (url: string, key: string, accessToken?: string) => SupabaseJsClient;
|
|
@@ -42,7 +42,7 @@ export type VerifyAccessTokenConfig = {
|
|
|
42
42
|
/**
|
|
43
43
|
* Establishes who the caller is, from a token whose signature is actually checked.
|
|
44
44
|
*
|
|
45
|
-
* Identity comes from the verified JWT and tenancy comes from `public.users`
|
|
45
|
+
* Identity comes from the verified JWT and tenancy comes from `public.users` - never from
|
|
46
46
|
* `user_metadata`, which the end user can rewrite through GoTrue. The profile read runs under the
|
|
47
47
|
* caller's own RLS (a user may read their own row), so the `organization_id` returned here is the
|
|
48
48
|
* same one `current_user_organization_id()` gives every policy.
|
package/dist/auth.js
CHANGED
|
@@ -4,8 +4,8 @@ exports.verifyAccessToken = exports.createRawClient = void 0;
|
|
|
4
4
|
const supabase_js_1 = require("@supabase/supabase-js");
|
|
5
5
|
const errors_1 = require("./errors");
|
|
6
6
|
/**
|
|
7
|
-
* A raw supabase-js client, built the one way the SDK ever needs one: server-side and stateless
|
|
8
|
-
* nothing is stored, refreshed or read out of a URL
|
|
7
|
+
* A raw supabase-js client, built the one way the SDK ever needs one: server-side and stateless -
|
|
8
|
+
* nothing is stored, refreshed or read out of a URL - carrying the caller's bearer token when
|
|
9
9
|
* there is one, so RLS sees them.
|
|
10
10
|
*/
|
|
11
11
|
const createRawClient = (url, key, accessToken) => (0, supabase_js_1.createClient)(url, key, {
|
|
@@ -21,7 +21,7 @@ const authError = (message) => new errors_1.SupabaseClientError({ status: 401, e
|
|
|
21
21
|
/**
|
|
22
22
|
* Establishes who the caller is, from a token whose signature is actually checked.
|
|
23
23
|
*
|
|
24
|
-
* Identity comes from the verified JWT and tenancy comes from `public.users`
|
|
24
|
+
* Identity comes from the verified JWT and tenancy comes from `public.users` - never from
|
|
25
25
|
* `user_metadata`, which the end user can rewrite through GoTrue. The profile read runs under the
|
|
26
26
|
* caller's own RLS (a user may read their own row), so the `organization_id` returned here is the
|
|
27
27
|
* same one `current_user_organization_id()` gives every policy.
|
package/dist/billing/dto.js
CHANGED
|
@@ -85,7 +85,7 @@ __decorate([
|
|
|
85
85
|
__metadata("design:type", String)
|
|
86
86
|
], PaymentIdDto.prototype, "paymentId", void 0);
|
|
87
87
|
// `status_in` and `type_in` arrive as CSV strings and are transformed to enum values, so
|
|
88
|
-
// they are omitted from the implemented contract
|
|
88
|
+
// they are omitted from the implemented contract - the same exception GetInvoicesQueryDto makes.
|
|
89
89
|
class GetPaymentsQueryDto {
|
|
90
90
|
page = 1;
|
|
91
91
|
page_size = 25;
|
package/dist/billing/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export * from './dto';
|
|
|
8
8
|
/**
|
|
9
9
|
* The readable half of the ledger. `billing_transactions` and `billing_events` are org-scoped
|
|
10
10
|
* for READ, and the catalog is readable by `anon`, so all of this runs perfectly well on the
|
|
11
|
-
* caller's own client
|
|
11
|
+
* caller's own client - and should, so that RLS is what scopes it rather than a filter the api
|
|
12
12
|
* remembered to add.
|
|
13
13
|
*/
|
|
14
14
|
export declare class BillingReadsResource {
|
|
@@ -24,15 +24,15 @@ export declare class BillingReadsResource {
|
|
|
24
24
|
* status. RLS scopes the rows; the eq is belt-and-braces.
|
|
25
25
|
*/
|
|
26
26
|
listPayments(query: GetPaymentsQueryDto): Promise<MmsList<PaymentSummary>>;
|
|
27
|
-
/** The full payment record, or null
|
|
27
|
+
/** The full payment record, or null - RLS makes another tenant's id look missing. */
|
|
28
28
|
paymentById(paymentId: string): Promise<PaymentEntity | null>;
|
|
29
29
|
/** Whether a catalog module is still on sale. The catalog is public, not tenant-scoped. */
|
|
30
30
|
isModuleActive(moduleId: string): Promise<boolean>;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* The ledger writes, on the service client only. Every write policy on `billing_transactions`
|
|
34
|
-
* and `billing_events` is `false` for `authenticated`
|
|
35
|
-
* service and by webhooks, never by a caller
|
|
34
|
+
* and `billing_events` is `false` for `authenticated` - the ledger is written by the billing
|
|
35
|
+
* service and by webhooks, never by a caller - so these are deliberately not reachable from
|
|
36
36
|
* `SupabaseClient`.
|
|
37
37
|
*/
|
|
38
38
|
export declare class BillingResource extends BillingReadsResource {
|
package/dist/billing/index.js
CHANGED
|
@@ -25,7 +25,7 @@ const BILLING_TRANSACTIONS_TABLE = {
|
|
|
25
25
|
table: 'billing_transactions',
|
|
26
26
|
entityType: enum_1.AuditTrailEntityType.BILLING_TRANSACTIONS,
|
|
27
27
|
};
|
|
28
|
-
/** The wire shape of a payment list row
|
|
28
|
+
/** The wire shape of a payment list row - never the internal bigint id, never the checkout secrets. */
|
|
29
29
|
const PAYMENT_SUMMARY_SELECT = 'mms_id, created_at, transaction_type, transaction_status, amount_minor, currency, paid_at, failure_reason';
|
|
30
30
|
/**
|
|
31
31
|
* PaymentEntity, column for column, plus the renewal's invoice through the unique
|
|
@@ -36,7 +36,7 @@ const PAYMENT_ENTITY_SELECT = `${PAYMENT_SUMMARY_SELECT}, updated_at, active_sea
|
|
|
36
36
|
/**
|
|
37
37
|
* The readable half of the ledger. `billing_transactions` and `billing_events` are org-scoped
|
|
38
38
|
* for READ, and the catalog is readable by `anon`, so all of this runs perfectly well on the
|
|
39
|
-
* caller's own client
|
|
39
|
+
* caller's own client - and should, so that RLS is what scopes it rather than a filter the api
|
|
40
40
|
* remembered to add.
|
|
41
41
|
*/
|
|
42
42
|
class BillingReadsResource {
|
|
@@ -97,7 +97,7 @@ class BillingReadsResource {
|
|
|
97
97
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
98
98
|
return (0, transforms_1.toMmsList)(data, page, pageSize, count);
|
|
99
99
|
}
|
|
100
|
-
/** The full payment record, or null
|
|
100
|
+
/** The full payment record, or null - RLS makes another tenant's id look missing. */
|
|
101
101
|
async paymentById(paymentId) {
|
|
102
102
|
const { data, error } = await this.supabase
|
|
103
103
|
.from('billing_transactions')
|
|
@@ -130,8 +130,8 @@ class BillingReadsResource {
|
|
|
130
130
|
exports.BillingReadsResource = BillingReadsResource;
|
|
131
131
|
/**
|
|
132
132
|
* The ledger writes, on the service client only. Every write policy on `billing_transactions`
|
|
133
|
-
* and `billing_events` is `false` for `authenticated`
|
|
134
|
-
* service and by webhooks, never by a caller
|
|
133
|
+
* and `billing_events` is `false` for `authenticated` - the ledger is written by the billing
|
|
134
|
+
* service and by webhooks, never by a caller - so these are deliberately not reachable from
|
|
135
135
|
* `SupabaseClient`.
|
|
136
136
|
*/
|
|
137
137
|
class BillingResource extends BillingReadsResource {
|
|
@@ -24,7 +24,7 @@ __decorate([
|
|
|
24
24
|
__metadata("design:type", String)
|
|
25
25
|
], InvoiceIdDto.prototype, "invoiceId", void 0);
|
|
26
26
|
// `status_in` arrives as a CSV string and is transformed to enum values, so it is omitted
|
|
27
|
-
// from the implemented contract
|
|
27
|
+
// from the implemented contract - the same exception GetUsersQueryDto makes for active_in.
|
|
28
28
|
class GetInvoicesQueryDto {
|
|
29
29
|
page = 1;
|
|
30
30
|
page_size = 25;
|
|
@@ -6,7 +6,7 @@ import type { GetInvoicesQueryDto } from './dto';
|
|
|
6
6
|
export * from './dto';
|
|
7
7
|
/**
|
|
8
8
|
* The readable half. `invoices` is SELECT-only for `authenticated` with an org-predicate
|
|
9
|
-
* policy
|
|
9
|
+
* policy - the same posture as the ledger it mirrors - so reads run on the caller's own
|
|
10
10
|
* client and RLS is what scopes them.
|
|
11
11
|
*/
|
|
12
12
|
export declare class InvoicesReadsResource {
|
|
@@ -15,14 +15,14 @@ export declare class InvoicesReadsResource {
|
|
|
15
15
|
constructor(supabase: TypedSupabaseClient, actor: AuditActor);
|
|
16
16
|
/** The organization's invoices, newest first. RLS scopes the rows; the eq is belt-and-braces. */
|
|
17
17
|
list(query: GetInvoicesQueryDto): Promise<MmsList<InvoiceSummary>>;
|
|
18
|
-
/** The full invoice document data, or null
|
|
18
|
+
/** The full invoice document data, or null - RLS makes another tenant's id look missing. */
|
|
19
19
|
byId(invoiceId: string): Promise<InvoiceEntity | null>;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* The document writes, on the service client only
|
|
22
|
+
* The document writes, on the service client only - every write policy on `invoices` is
|
|
23
23
|
* `false` for `authenticated`. Rows are written by the api's billing service beside a
|
|
24
|
-
* renewal's ledger write
|
|
25
|
-
* status after it is issued
|
|
24
|
+
* renewal's ledger write - a renewal is inserted already paid, so an invoice never moves
|
|
25
|
+
* status after it is issued - never by a caller.
|
|
26
26
|
*/
|
|
27
27
|
export declare class InvoicesResource extends InvoicesReadsResource {
|
|
28
28
|
private readonly invoices;
|
|
@@ -32,7 +32,7 @@ export declare class InvoicesResource extends InvoicesReadsResource {
|
|
|
32
32
|
/**
|
|
33
33
|
* The organization's highest invoice number, 0 when it has none. The api assigns
|
|
34
34
|
* `max + 1` and lets the `(organization_id, invoice_number)` unique index arbitrate a
|
|
35
|
-
* concurrent insert
|
|
35
|
+
* concurrent insert - the retry lives there, not here.
|
|
36
36
|
*/
|
|
37
37
|
maxNumber(): Promise<number>;
|
|
38
38
|
}
|
|
@@ -24,13 +24,13 @@ const INVOICES_TABLE = {
|
|
|
24
24
|
table: 'invoices',
|
|
25
25
|
entityType: enum_1.AuditTrailEntityType.INVOICE,
|
|
26
26
|
};
|
|
27
|
-
/** The wire shape of an invoice list row
|
|
27
|
+
/** The wire shape of an invoice list row - never the internal bigint id. */
|
|
28
28
|
const INVOICE_SUMMARY_SELECT = 'mms_id, invoice_number, transaction_type, status, issued_at, paid_at, amount_minor, currency';
|
|
29
29
|
/** InvoiceEntity, column for column: the summary plus the frozen document data. */
|
|
30
30
|
const INVOICE_ENTITY_SELECT = `${INVOICE_SUMMARY_SELECT}, billing_transaction_id, paystack_reference, period_start, period_end, line_items, billed_to`;
|
|
31
31
|
/**
|
|
32
32
|
* The readable half. `invoices` is SELECT-only for `authenticated` with an org-predicate
|
|
33
|
-
* policy
|
|
33
|
+
* policy - the same posture as the ledger it mirrors - so reads run on the caller's own
|
|
34
34
|
* client and RLS is what scopes them.
|
|
35
35
|
*/
|
|
36
36
|
class InvoicesReadsResource {
|
|
@@ -61,7 +61,7 @@ class InvoicesReadsResource {
|
|
|
61
61
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
62
62
|
return (0, transforms_1.toMmsList)(data, page, pageSize, count);
|
|
63
63
|
}
|
|
64
|
-
/** The full invoice document data, or null
|
|
64
|
+
/** The full invoice document data, or null - RLS makes another tenant's id look missing. */
|
|
65
65
|
async byId(invoiceId) {
|
|
66
66
|
const { data, error } = await this.supabase
|
|
67
67
|
.from('invoices')
|
|
@@ -76,10 +76,10 @@ class InvoicesReadsResource {
|
|
|
76
76
|
}
|
|
77
77
|
exports.InvoicesReadsResource = InvoicesReadsResource;
|
|
78
78
|
/**
|
|
79
|
-
* The document writes, on the service client only
|
|
79
|
+
* The document writes, on the service client only - every write policy on `invoices` is
|
|
80
80
|
* `false` for `authenticated`. Rows are written by the api's billing service beside a
|
|
81
|
-
* renewal's ledger write
|
|
82
|
-
* status after it is issued
|
|
81
|
+
* renewal's ledger write - a renewal is inserted already paid, so an invoice never moves
|
|
82
|
+
* status after it is issued - never by a caller.
|
|
83
83
|
*/
|
|
84
84
|
class InvoicesResource extends InvoicesReadsResource {
|
|
85
85
|
invoices;
|
|
@@ -94,7 +94,7 @@ class InvoicesResource extends InvoicesReadsResource {
|
|
|
94
94
|
/**
|
|
95
95
|
* The organization's highest invoice number, 0 when it has none. The api assigns
|
|
96
96
|
* `max + 1` and lets the `(organization_id, invoice_number)` unique index arbitrate a
|
|
97
|
-
* concurrent insert
|
|
97
|
+
* concurrent insert - the retry lives there, not here.
|
|
98
98
|
*/
|
|
99
99
|
async maxNumber() {
|
|
100
100
|
const { data, error } = await this.supabase
|