@managemint-solutions/sdk 0.33.0 → 0.35.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/README.md +51 -2
- package/dist/cache/index.d.ts +41 -0
- package/dist/cache/index.js +59 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +4 -0
- package/dist/configs/dto.d.ts +11 -0
- package/dist/configs/dto.js +73 -0
- package/dist/configs/index.d.ts +25 -0
- package/dist/configs/index.js +101 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -2
- package/dist/service-client/index.d.ts +2 -0
- package/dist/service-client/index.js +4 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -18,10 +18,11 @@ are peer dependencies — the request DTOs (`AddStatusDto`, `UpdateStatusDto`, `
|
|
|
18
18
|
`UpdateProjectDto`, `GetTasksDto`, `TaskIdDto`, `CreateTaskDto`, `UpdateTaskDto`,
|
|
19
19
|
`GetAuditTrailDto`, `SignInDto`,
|
|
20
20
|
`ExchangeRefreshTokenDto`, `SendPasswordResetEmailDto`, `ExchangeTokenHashDto`,
|
|
21
|
-
`ResetPasswordDto`) ship with their class-validator
|
|
21
|
+
`ResetPasswordDto`, `CreateConfigDto`, `UpdateConfigDto`) ship with their class-validator
|
|
22
22
|
decorators so consumers validate against
|
|
23
23
|
the same rules the SDK's input types describe. `@nestjs/common` (^11) is an optional peer
|
|
24
|
-
dependency, needed only for the `@managemint-solutions/sdk/nest` subpath
|
|
24
|
+
dependency, needed only for the `@managemint-solutions/sdk/nest` subpath, and `@upstash/redis`
|
|
25
|
+
(^1.38) is an optional peer needed only for the `@managemint-solutions/sdk/cache` subpath.
|
|
25
26
|
|
|
26
27
|
## Usage
|
|
27
28
|
|
|
@@ -378,6 +379,27 @@ const total = monthlyTotalMinor(pricebook, ['CRM'], seatCount);
|
|
|
378
379
|
Each pricebook comes back with its `modules` flattened to `PricedModule`s (the `pricebook_modules`
|
|
379
380
|
row joined with its `billing_modules` catalog entry). Exactly one pricebook is active at a time.
|
|
380
381
|
|
|
382
|
+
## Configs
|
|
383
|
+
|
|
384
|
+
`public.configs` holds global named JSON configs — one `value` object per row, with no
|
|
385
|
+
organization on it. Every signed-in caller may read, and only the service role may write, so
|
|
386
|
+
`configs` hangs off the caller's client and the service client alike and a write on the former
|
|
387
|
+
fails at the database (Scott edits the rows in Supabase Studio for now). A feature-flag set is
|
|
388
|
+
just a row someone named, not a concept the SDK knows about, and the table is not audited.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
import type { ConfigEntity } from '@managemint-solutions/entities/configs';
|
|
392
|
+
|
|
393
|
+
const all: ConfigEntity[] = await supabase.configs.list(); // user client reads
|
|
394
|
+
const flags = await supabase.configs.getByName('feature_flags'); // whatever rows exist in Studio
|
|
395
|
+
const one = await supabase.configs.getById(flags.mms_id);
|
|
396
|
+
|
|
397
|
+
const service = createSupabaseServiceClient({ url, serviceRoleKey });
|
|
398
|
+
await service.configs.create({ name: 'maintenance', description: 'Planned downtime banner' });
|
|
399
|
+
await service.configs.update('feature_flags', { value: { ...flags.value, timesheets: false } }); // replaces the object, stamps updated_at
|
|
400
|
+
await service.configs.remove('maintenance'); // hard delete, service role only
|
|
401
|
+
```
|
|
402
|
+
|
|
381
403
|
## Errors
|
|
382
404
|
|
|
383
405
|
Every error the SDK throws is a `SupabaseClientError` carrying the HTTP `status` to respond
|
|
@@ -391,6 +413,33 @@ missing JWT is `401 Authentication Error`. Consumers return these as-is instead
|
|
|
391
413
|
database errors themselves, and `error.code` keeps the raw Postgres code for the few callers
|
|
392
414
|
that branch on one (the invoice-number retry on `23505`).
|
|
393
415
|
|
|
416
|
+
## Cache
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
import { createTaggedCache } from '@managemint-solutions/sdk/cache';
|
|
420
|
+
import { userMeCacheKey, userCacheTag, USER_ME_CACHE_TTL_SECONDS } from '@managemint-solutions/entities/users/cache';
|
|
421
|
+
|
|
422
|
+
const cache = createTaggedCache({ url: UPSTASH_REDIS_REST_URL, token: UPSTASH_REDIS_REST_TOKEN });
|
|
423
|
+
|
|
424
|
+
await cache.set(userMeCacheKey(mmsId), profile, { ttl: USER_ME_CACHE_TTL_SECONDS, tags: [userCacheTag(mmsId)] });
|
|
425
|
+
const hit = await cache.get<LoggedInUser>(userMeCacheKey(mmsId)); // null on a miss
|
|
426
|
+
await cache.invalidateTag([userCacheTag(mmsId)]);
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
A tag-aware key-value cache on Upstash Redis (REST), shared by the portal, which reads and
|
|
430
|
+
writes each member's own `GET /users/me`, and the api, which only expires it after a write.
|
|
431
|
+
Redis has no tags, so each tag is a set of the keys written under it, stored as `tag:<tag>`
|
|
432
|
+
and expiring with its newest member: `set` is one pipeline (`SET … EX ttl`, then `SADD` and
|
|
433
|
+
`EXPIRE` per tag), `invalidateTag` is two round trips (`SMEMBERS` per tag, then one `DEL` of the
|
|
434
|
+
members and the sets). Values are JSON-serialised by the client; `get<T>` is the caller's
|
|
435
|
+
promise about what was written, not a check.
|
|
436
|
+
|
|
437
|
+
The client takes its URL and token as config — the SDK reads no environment variables — and
|
|
438
|
+
retries once with a three-second timeout per request. It **throws** on a failed call: what a
|
|
439
|
+
failure means is the caller's decision, and both callers today fail open (a miss on the portal,
|
|
440
|
+
a Sentry report on the api). The key, tag and TTL contract for the profile entry lives in
|
|
441
|
+
`@managemint-solutions/entities/users/cache`, so the tagging is one edit for both sides.
|
|
442
|
+
|
|
394
443
|
## NestJS
|
|
395
444
|
|
|
396
445
|
```ts
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tag-aware key-value cache on Upstash Redis, shared by the portal (which reads and writes
|
|
3
|
+
* profile entries) and the api (which only expires them).
|
|
4
|
+
*
|
|
5
|
+
* Redis has no tags of its own, so each tag is a set of the keys written under it, stored as
|
|
6
|
+
* `tag:<tag>`. Expiring a tag deletes every key in the set and the set itself. The sets expire
|
|
7
|
+
* with the entries they hold, so an entry that simply ages out leaves nothing behind.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately narrow: three calls and no knowledge of what is cached. The key, tag and TTL
|
|
10
|
+
* contract for the one thing cached today lives in `@managemint-solutions/entities/users/cache`.
|
|
11
|
+
*
|
|
12
|
+
* Like every other client in the SDK it takes its configuration as an argument and reads no
|
|
13
|
+
* environment variables. It throws on a failed call; the callers decide what a failure means,
|
|
14
|
+
* and both of them fail open.
|
|
15
|
+
*/
|
|
16
|
+
export type TaggedCacheConfig = {
|
|
17
|
+
/** The database's REST URL, `https://<name>.upstash.io`. */
|
|
18
|
+
url: string;
|
|
19
|
+
/** The database's REST token. */
|
|
20
|
+
token: string;
|
|
21
|
+
};
|
|
22
|
+
export type TaggedCacheSetOptions = {
|
|
23
|
+
/** Seconds until the entry expires on its own. */
|
|
24
|
+
ttl: number;
|
|
25
|
+
/** The tags a later `invalidateTag` may drop the entry under. */
|
|
26
|
+
tags?: string[];
|
|
27
|
+
};
|
|
28
|
+
export declare class TaggedCache {
|
|
29
|
+
private readonly redis;
|
|
30
|
+
constructor(config: TaggedCacheConfig);
|
|
31
|
+
/**
|
|
32
|
+
* The entry under `key`, or null when there is none. The value is JSON-parsed by the
|
|
33
|
+
* client; `T` is the caller's promise about what was written, not a check.
|
|
34
|
+
*/
|
|
35
|
+
get<T>(key: string): Promise<T | null>;
|
|
36
|
+
/** Writes `value` under `key` for `ttl` seconds, and files the key under each tag. One round trip. */
|
|
37
|
+
set(key: string, value: unknown, { ttl, tags }: TaggedCacheSetOptions): Promise<void>;
|
|
38
|
+
/** Deletes every entry filed under any of `tags`, and the tag sets themselves. Two round trips. */
|
|
39
|
+
invalidateTag(tags: string[]): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export declare const createTaggedCache: (config: TaggedCacheConfig) => TaggedCache;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTaggedCache = exports.TaggedCache = void 0;
|
|
4
|
+
const redis_1 = require("@upstash/redis");
|
|
5
|
+
/**
|
|
6
|
+
* How long one Upstash call may take before it is abandoned. Every call here sits on the
|
|
7
|
+
* request path - a portal boot on the read side, a profile write on the expiry side - so a
|
|
8
|
+
* stalled Redis must cost the caller a bounded wait and nothing more.
|
|
9
|
+
*/
|
|
10
|
+
const REQUEST_TIMEOUT_MS = 3_000;
|
|
11
|
+
/** One retry, briefly: the callers fail open, so patience only lengthens the miss. */
|
|
12
|
+
const RETRY = { retries: 1, backoff: () => 200 };
|
|
13
|
+
const tagSetKey = (tag) => `tag:${tag}`;
|
|
14
|
+
class TaggedCache {
|
|
15
|
+
redis;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.redis = new redis_1.Redis({
|
|
18
|
+
url: config.url,
|
|
19
|
+
token: config.token,
|
|
20
|
+
retry: RETRY,
|
|
21
|
+
// A function, so each request gets a fresh signal; one shared signal would be dead for
|
|
22
|
+
// every call after the first timeout.
|
|
23
|
+
signal: () => AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The entry under `key`, or null when there is none. The value is JSON-parsed by the
|
|
28
|
+
* client; `T` is the caller's promise about what was written, not a check.
|
|
29
|
+
*/
|
|
30
|
+
get(key) {
|
|
31
|
+
return this.redis.get(key);
|
|
32
|
+
}
|
|
33
|
+
/** Writes `value` under `key` for `ttl` seconds, and files the key under each tag. One round trip. */
|
|
34
|
+
async set(key, value, { ttl, tags = [] }) {
|
|
35
|
+
const pipeline = this.redis.pipeline().set(key, value, { ex: ttl });
|
|
36
|
+
for (const tag of tags) {
|
|
37
|
+
// The set lives at least as long as its newest member, so a member can never outlive
|
|
38
|
+
// the set that would be used to find it.
|
|
39
|
+
pipeline.sadd(tagSetKey(tag), key).expire(tagSetKey(tag), ttl);
|
|
40
|
+
}
|
|
41
|
+
await pipeline.exec();
|
|
42
|
+
}
|
|
43
|
+
/** Deletes every entry filed under any of `tags`, and the tag sets themselves. Two round trips. */
|
|
44
|
+
async invalidateTag(tags) {
|
|
45
|
+
if (tags.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const sets = tags.map(tagSetKey);
|
|
49
|
+
const read = this.redis.pipeline();
|
|
50
|
+
for (const set of sets) {
|
|
51
|
+
read.smembers(set);
|
|
52
|
+
}
|
|
53
|
+
const members = (await read.exec()).flat();
|
|
54
|
+
await this.redis.del(...new Set([...members, ...sets]));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.TaggedCache = TaggedCache;
|
|
58
|
+
const createTaggedCache = (config) => new TaggedCache(config);
|
|
59
|
+
exports.createTaggedCache = createTaggedCache;
|
package/dist/client.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type AuthContext } from './auth';
|
|
|
3
3
|
import { AuditResource, type AuditErrorHandler } from './audit';
|
|
4
4
|
import { AdditionalDetailsResource } from './additional-details';
|
|
5
5
|
import { ClientsResource } from './clients';
|
|
6
|
+
import { ConfigsResource } from './configs';
|
|
6
7
|
import { NotesResource } from './notes';
|
|
7
8
|
import { NotificationsResource } from './notifications';
|
|
8
9
|
import { BillingReadsResource } from './billing';
|
|
@@ -41,6 +42,7 @@ export declare class SupabaseClient {
|
|
|
41
42
|
readonly supabase: TypedSupabaseClient;
|
|
42
43
|
readonly auth: AuthContext;
|
|
43
44
|
readonly audit: AuditResource;
|
|
45
|
+
readonly configs: ConfigsResource;
|
|
44
46
|
readonly statuses: StatusesResource;
|
|
45
47
|
readonly additionalDetails: AdditionalDetailsResource;
|
|
46
48
|
readonly notes: NotesResource;
|
package/dist/client.js
CHANGED
|
@@ -5,6 +5,7 @@ const auth_1 = require("./auth");
|
|
|
5
5
|
const audit_1 = require("./audit");
|
|
6
6
|
const additional_details_1 = require("./additional-details");
|
|
7
7
|
const clients_1 = require("./clients");
|
|
8
|
+
const configs_1 = require("./configs");
|
|
8
9
|
const notes_1 = require("./notes");
|
|
9
10
|
const notifications_1 = require("./notifications");
|
|
10
11
|
const billing_1 = require("./billing");
|
|
@@ -25,6 +26,8 @@ class SupabaseClient {
|
|
|
25
26
|
supabase; // escape hatch for not-yet-migrated queries
|
|
26
27
|
auth;
|
|
27
28
|
audit;
|
|
29
|
+
// Reads run under the authenticated grant; the writes are the service client's.
|
|
30
|
+
configs;
|
|
28
31
|
statuses;
|
|
29
32
|
additionalDetails;
|
|
30
33
|
notes;
|
|
@@ -44,6 +47,7 @@ class SupabaseClient {
|
|
|
44
47
|
config.client ?? (0, auth_1.createRawClient)(config.url, config.key, config.accessToken);
|
|
45
48
|
// Built before the resources: every write they make goes through it.
|
|
46
49
|
this.audit = new audit_1.AuditResource(this.supabase, this.auth, config.onAuditError);
|
|
50
|
+
this.configs = new configs_1.ConfigsResource(this.supabase);
|
|
47
51
|
this.statuses = new statuses_1.StatusesResource(this.supabase, this.auth, this.audit);
|
|
48
52
|
this.additionalDetails = new additional_details_1.AdditionalDetailsResource(this.supabase, this.auth, this.audit);
|
|
49
53
|
this.notes = new notes_1.NotesResource(this.supabase, this.auth, this.audit);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConfigValue } from '@managemint-solutions/entities/configs';
|
|
2
|
+
import type { CreateConfigDto as CreateConfigDtoType, UpdateConfigDto as UpdateConfigDtoType } from '@managemint-solutions/entities/configs/dto';
|
|
3
|
+
export declare class CreateConfigDto implements CreateConfigDtoType {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly description?: string;
|
|
6
|
+
readonly value?: ConfigValue;
|
|
7
|
+
}
|
|
8
|
+
export declare class UpdateConfigDto implements UpdateConfigDtoType {
|
|
9
|
+
readonly description?: string;
|
|
10
|
+
readonly value?: ConfigValue;
|
|
11
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.UpdateConfigDto = exports.CreateConfigDto = void 0;
|
|
13
|
+
const class_validator_1 = require("class-validator");
|
|
14
|
+
const class_transformer_1 = require("class-transformer");
|
|
15
|
+
// class-validator version of the shared DTO type from @managemint-solutions/entities, so the
|
|
16
|
+
// wire contract cannot drift.
|
|
17
|
+
/**
|
|
18
|
+
* Whitespace on either side of a pasted value is not part of the answer. Trimming here rather
|
|
19
|
+
* than in the api means the length rules below measure what will actually be stored — a name of
|
|
20
|
+
* three spaces is empty, not three characters long.
|
|
21
|
+
*/
|
|
22
|
+
const Trim = () => (0, class_transformer_1.Transform)(({ value }) => (typeof value === 'string' ? value.trim() : value), {
|
|
23
|
+
toClassOnly: true,
|
|
24
|
+
});
|
|
25
|
+
class CreateConfigDto {
|
|
26
|
+
name;
|
|
27
|
+
description;
|
|
28
|
+
// This DTO is the only place the object shape is enforced — the column has no CHECK by
|
|
29
|
+
// design, so a config may hold whatever keys the row's author named. class-validator's
|
|
30
|
+
// isObject rejects arrays and scalars, which is exactly the line: one JSON object, nothing
|
|
31
|
+
// else. `@ValidateIf` rather than `@IsOptional` because the latter skips an explicit null
|
|
32
|
+
// too, and null is not a config value — an omitted `value` becomes `{}` at the insert.
|
|
33
|
+
value;
|
|
34
|
+
}
|
|
35
|
+
exports.CreateConfigDto = CreateConfigDto;
|
|
36
|
+
__decorate([
|
|
37
|
+
Trim(),
|
|
38
|
+
(0, class_validator_1.IsString)({ message: 'Invalid name' }),
|
|
39
|
+
(0, class_validator_1.MinLength)(1, { message: 'Name cannot be empty' }),
|
|
40
|
+
(0, class_validator_1.MaxLength)(100, { message: 'Name cannot exceed 100 characters' }),
|
|
41
|
+
__metadata("design:type", String)
|
|
42
|
+
], CreateConfigDto.prototype, "name", void 0);
|
|
43
|
+
__decorate([
|
|
44
|
+
(0, class_validator_1.IsOptional)(),
|
|
45
|
+
Trim(),
|
|
46
|
+
(0, class_validator_1.IsString)({ message: 'Invalid description' }),
|
|
47
|
+
(0, class_validator_1.MaxLength)(1000, { message: 'Description cannot exceed 1000 characters' }),
|
|
48
|
+
__metadata("design:type", String)
|
|
49
|
+
], CreateConfigDto.prototype, "description", void 0);
|
|
50
|
+
__decorate([
|
|
51
|
+
(0, class_validator_1.ValidateIf)((o) => o.value !== undefined),
|
|
52
|
+
(0, class_validator_1.IsObject)({ message: 'Value must be a JSON object' }),
|
|
53
|
+
__metadata("design:type", Object)
|
|
54
|
+
], CreateConfigDto.prototype, "value", void 0);
|
|
55
|
+
class UpdateConfigDto {
|
|
56
|
+
description;
|
|
57
|
+
// Same rule as the create: an absent `value` leaves the column alone, but a null is a value
|
|
58
|
+
// the column cannot hold. It replaces the whole object rather than merging into it.
|
|
59
|
+
value;
|
|
60
|
+
}
|
|
61
|
+
exports.UpdateConfigDto = UpdateConfigDto;
|
|
62
|
+
__decorate([
|
|
63
|
+
(0, class_validator_1.IsOptional)(),
|
|
64
|
+
Trim(),
|
|
65
|
+
(0, class_validator_1.IsString)({ message: 'Invalid description' }),
|
|
66
|
+
(0, class_validator_1.MaxLength)(1000, { message: 'Description cannot exceed 1000 characters' }),
|
|
67
|
+
__metadata("design:type", String)
|
|
68
|
+
], UpdateConfigDto.prototype, "description", void 0);
|
|
69
|
+
__decorate([
|
|
70
|
+
(0, class_validator_1.ValidateIf)((o) => o.value !== undefined),
|
|
71
|
+
(0, class_validator_1.IsObject)({ message: 'Value must be a JSON object' }),
|
|
72
|
+
__metadata("design:type", Object)
|
|
73
|
+
], UpdateConfigDto.prototype, "value", void 0);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ConfigEntity } from '@managemint-solutions/entities/configs';
|
|
2
|
+
import type { CreateConfigDto as CreateConfigDtoType, UpdateConfigDto as UpdateConfigDtoType } from '@managemint-solutions/entities/configs/dto';
|
|
3
|
+
import type { TypedSupabaseClient } from '../client';
|
|
4
|
+
export * from './dto';
|
|
5
|
+
/**
|
|
6
|
+
* Global named JSON configs. Every signed-in caller may read (the READ policy is `true` — there
|
|
7
|
+
* is no tenant on the row), so this hangs off the user client and the service client alike;
|
|
8
|
+
* only the service role may write, and a write on the user client fails at the database.
|
|
9
|
+
* Not audited: Studio is the other writer, and nothing would see its edits anyway.
|
|
10
|
+
*/
|
|
11
|
+
export declare class ConfigsResource {
|
|
12
|
+
private readonly supabase;
|
|
13
|
+
constructor(supabase: TypedSupabaseClient);
|
|
14
|
+
list(): Promise<ConfigEntity[]>;
|
|
15
|
+
getByName(name: string): Promise<ConfigEntity>;
|
|
16
|
+
getById(configId: string): Promise<ConfigEntity>;
|
|
17
|
+
create(input: CreateConfigDtoType): Promise<ConfigEntity>;
|
|
18
|
+
/** `value` replaces the whole object — it is not merged. */
|
|
19
|
+
update(name: string, patch: UpdateConfigDtoType): Promise<ConfigEntity>;
|
|
20
|
+
/**
|
|
21
|
+
* Hard delete — there is nothing to soft-delete into and nothing references a config row.
|
|
22
|
+
* Service role only, like every write here; a call on the user client fails at the database.
|
|
23
|
+
*/
|
|
24
|
+
remove(name: string): Promise<void>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.ConfigsResource = void 0;
|
|
18
|
+
const errors_1 = require("../errors");
|
|
19
|
+
__exportStar(require("./dto"), exports);
|
|
20
|
+
// The public contract is ConfigEntity from @managemint-solutions/entities; the casts at the
|
|
21
|
+
// returns declare the shape this select string produces (columns defined in the api's
|
|
22
|
+
// supabase/migrations). `id` never leaves SQL.
|
|
23
|
+
const CONFIG_SELECT = 'mms_id, created_at, updated_at, name, description, value';
|
|
24
|
+
/**
|
|
25
|
+
* Global named JSON configs. Every signed-in caller may read (the READ policy is `true` — there
|
|
26
|
+
* is no tenant on the row), so this hangs off the user client and the service client alike;
|
|
27
|
+
* only the service role may write, and a write on the user client fails at the database.
|
|
28
|
+
* Not audited: Studio is the other writer, and nothing would see its edits anyway.
|
|
29
|
+
*/
|
|
30
|
+
class ConfigsResource {
|
|
31
|
+
supabase;
|
|
32
|
+
constructor(supabase) {
|
|
33
|
+
this.supabase = supabase;
|
|
34
|
+
}
|
|
35
|
+
async list() {
|
|
36
|
+
const { data, error } = await this.supabase
|
|
37
|
+
.from('configs')
|
|
38
|
+
.select(CONFIG_SELECT)
|
|
39
|
+
.order('name');
|
|
40
|
+
if (error)
|
|
41
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
42
|
+
return (data ?? []);
|
|
43
|
+
}
|
|
44
|
+
async getByName(name) {
|
|
45
|
+
const { data, error } = await this.supabase
|
|
46
|
+
.from('configs')
|
|
47
|
+
.select(CONFIG_SELECT)
|
|
48
|
+
.eq('name', name)
|
|
49
|
+
.single();
|
|
50
|
+
if (error)
|
|
51
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
async getById(configId) {
|
|
55
|
+
const { data, error } = await this.supabase
|
|
56
|
+
.from('configs')
|
|
57
|
+
.select(CONFIG_SELECT)
|
|
58
|
+
.eq('mms_id', configId)
|
|
59
|
+
.single();
|
|
60
|
+
if (error)
|
|
61
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
async create(input) {
|
|
65
|
+
const { data, error } = await this.supabase
|
|
66
|
+
.from('configs')
|
|
67
|
+
.insert({
|
|
68
|
+
name: input.name,
|
|
69
|
+
description: input.description ?? '',
|
|
70
|
+
value: input.value ?? {},
|
|
71
|
+
created_at: new Date().toISOString(),
|
|
72
|
+
})
|
|
73
|
+
.select(CONFIG_SELECT)
|
|
74
|
+
.single();
|
|
75
|
+
if (error)
|
|
76
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
79
|
+
/** `value` replaces the whole object — it is not merged. */
|
|
80
|
+
async update(name, patch) {
|
|
81
|
+
const { data, error } = await this.supabase
|
|
82
|
+
.from('configs')
|
|
83
|
+
.update({ ...patch, updated_at: new Date().toISOString() })
|
|
84
|
+
.eq('name', name)
|
|
85
|
+
.select(CONFIG_SELECT)
|
|
86
|
+
.single();
|
|
87
|
+
if (error)
|
|
88
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Hard delete — there is nothing to soft-delete into and nothing references a config row.
|
|
93
|
+
* Service role only, like every write here; a call on the user client fails at the database.
|
|
94
|
+
*/
|
|
95
|
+
async remove(name) {
|
|
96
|
+
const { error } = await this.supabase.from('configs').delete().eq('name', name);
|
|
97
|
+
if (error)
|
|
98
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.ConfigsResource = ConfigsResource;
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { CancelSubscriptionDto, GetPaymentsQueryDto, InitialPaymentDto, PaymentI
|
|
|
15
15
|
export { GetInvoicesQueryDto, InvoiceIdDto } from './billing/invoices';
|
|
16
16
|
export type { CreateFeatureRequestRow, CreatedFeatureRequest } from './feature-requests';
|
|
17
17
|
export { CreateFeatureRequestDto } from './feature-requests';
|
|
18
|
+
export { CreateConfigDto, UpdateConfigDto } from './configs';
|
|
18
19
|
export { CreateSupportRequestDto } from './support';
|
|
19
20
|
export { AddStatusDto, GetStatusesDto, StatusIdDto, UpdateStatusDto } from './statuses';
|
|
20
21
|
export { AddAdditionalDetailsDto, AdditionalDetailsIdDto, GetAdditionalDetailsDto, IsAdditionalDetailsValueByType, UpdateAdditionalDetailsDto, } from './additional-details';
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
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.monthlyTotalMinor = exports.UpdateTaskDto = exports.TaskIdDto = exports.GetTasksDto = exports.CreateTaskDto = exports.UpdateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = exports.CreateProjectDto = exports.UpdateClientDto = exports.GetClientsDto = exports.CreateClientDto = exports.ClientIdDto = void 0;
|
|
10
|
+
exports.UserIdDto = exports.UpdateUserDto = exports.UpdateMyProfileDto = exports.GetUsersQueryDto = exports.AddUserDto = exports.UpdatePermissionsDto = exports.PermissionsResource = exports.UpdateOrganizationDto = exports.CreateOrganizationDto = exports.NotificationIdDto = exports.GetNotificationsDto = exports.NoteIdDto = exports.GetNotesDto = exports.AddNoteDto = exports.UpdateAdditionalDetailsDto = exports.IsAdditionalDetailsValueByType = exports.GetAdditionalDetailsDto = exports.AdditionalDetailsIdDto = exports.AddAdditionalDetailsDto = exports.UpdateStatusDto = exports.StatusIdDto = exports.GetStatusesDto = exports.AddStatusDto = exports.CreateSupportRequestDto = exports.UpdateConfigDto = exports.CreateConfigDto = exports.CreateFeatureRequestDto = exports.InvoiceIdDto = exports.GetInvoicesQueryDto = exports.ReinstateSubscriptionDto = exports.PurchaseModuleDto = exports.PaymentIdDto = exports.InitialPaymentDto = exports.GetPaymentsQueryDto = exports.CancelSubscriptionDto = exports.GetAuditTrailDto = exports.IsNullable = exports.EmptyToNull = exports.SupabaseClientError = exports.createSupabaseServiceClient = exports.SupabaseServiceClient = exports.SignInDto = exports.SendPasswordResetEmailDto = exports.ResetPasswordDto = exports.ExchangeTokenHashDto = exports.ExchangeRefreshTokenDto = exports.createSupabaseAuthClient = exports.SupabaseAuthClient = exports.createSupabaseClient = exports.SupabaseClient = void 0;
|
|
11
|
+
exports.monthlyTotalMinor = exports.UpdateTaskDto = exports.TaskIdDto = exports.GetTasksDto = exports.CreateTaskDto = exports.UpdateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = exports.CreateProjectDto = exports.UpdateClientDto = exports.GetClientsDto = exports.CreateClientDto = exports.ClientIdDto = exports.SupportingFileIdDto = exports.GetSupportingFilesDto = void 0;
|
|
12
12
|
var client_1 = require("./client");
|
|
13
13
|
Object.defineProperty(exports, "SupabaseClient", { enumerable: true, get: function () { return client_1.SupabaseClient; } });
|
|
14
14
|
Object.defineProperty(exports, "createSupabaseClient", { enumerable: true, get: function () { return client_1.createSupabaseClient; } });
|
|
@@ -43,6 +43,9 @@ Object.defineProperty(exports, "GetInvoicesQueryDto", { enumerable: true, get: f
|
|
|
43
43
|
Object.defineProperty(exports, "InvoiceIdDto", { enumerable: true, get: function () { return invoices_1.InvoiceIdDto; } });
|
|
44
44
|
var feature_requests_1 = require("./feature-requests");
|
|
45
45
|
Object.defineProperty(exports, "CreateFeatureRequestDto", { enumerable: true, get: function () { return feature_requests_1.CreateFeatureRequestDto; } });
|
|
46
|
+
var configs_1 = require("./configs");
|
|
47
|
+
Object.defineProperty(exports, "CreateConfigDto", { enumerable: true, get: function () { return configs_1.CreateConfigDto; } });
|
|
48
|
+
Object.defineProperty(exports, "UpdateConfigDto", { enumerable: true, get: function () { return configs_1.UpdateConfigDto; } });
|
|
46
49
|
var support_1 = require("./support");
|
|
47
50
|
Object.defineProperty(exports, "CreateSupportRequestDto", { enumerable: true, get: function () { return support_1.CreateSupportRequestDto; } });
|
|
48
51
|
var statuses_1 = require("./statuses");
|
|
@@ -2,6 +2,7 @@ import { AuditResource, type AuditActor, type AuditedTable, type AuditedTableOpt
|
|
|
2
2
|
import type { TypedSupabaseClient } from '../client';
|
|
3
3
|
import { BillingResource } from '../billing';
|
|
4
4
|
import { InvoicesResource } from '../billing/invoices';
|
|
5
|
+
import { ConfigsResource } from '../configs';
|
|
5
6
|
import { FeatureRequestsResource } from '../feature-requests';
|
|
6
7
|
import { NotificationsOutboxResource } from '../notifications';
|
|
7
8
|
import { OrganizationInvitesResource } from '../organization-invites';
|
|
@@ -57,6 +58,7 @@ export declare class SupabaseServiceClient {
|
|
|
57
58
|
readonly supabase: TypedSupabaseClient;
|
|
58
59
|
readonly notifications: NotificationsOutboxResource;
|
|
59
60
|
readonly featureRequests: FeatureRequestsResource;
|
|
61
|
+
readonly configs: ConfigsResource;
|
|
60
62
|
readonly signup: OrganizationSignupResource;
|
|
61
63
|
readonly paystackEvents: WebhookEventsResource;
|
|
62
64
|
readonly resendEvents: WebhookEventsResource;
|
|
@@ -6,6 +6,7 @@ const audit_1 = require("../audit");
|
|
|
6
6
|
const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
7
7
|
const billing_1 = require("../billing");
|
|
8
8
|
const invoices_1 = require("../billing/invoices");
|
|
9
|
+
const configs_1 = require("../configs");
|
|
9
10
|
const feature_requests_1 = require("../feature-requests");
|
|
10
11
|
const notifications_1 = require("../notifications");
|
|
11
12
|
const errors_1 = require("../errors");
|
|
@@ -71,6 +72,8 @@ class SupabaseServiceClient {
|
|
|
71
72
|
// Tenant-less, like `signup`: a feature request comes from a visitor with no account, so it
|
|
72
73
|
// hangs off the client rather than off an organization scope.
|
|
73
74
|
featureRequests;
|
|
75
|
+
// Global, with no organization on the row, so it hangs off the client rather than a scope.
|
|
76
|
+
configs;
|
|
74
77
|
signup;
|
|
75
78
|
paystackEvents;
|
|
76
79
|
resendEvents;
|
|
@@ -80,6 +83,7 @@ class SupabaseServiceClient {
|
|
|
80
83
|
this.paystackEvents = new webhook_events_1.WebhookEventsResource(this.supabase, 'paystack_webhook_events');
|
|
81
84
|
this.resendEvents = new webhook_events_1.WebhookEventsResource(this.supabase, 'resend_webhook_events');
|
|
82
85
|
this.featureRequests = new feature_requests_1.FeatureRequestsResource(this.supabase);
|
|
86
|
+
this.configs = new configs_1.ConfigsResource(this.supabase);
|
|
83
87
|
this.signup = new organizations_1.OrganizationSignupResource(this.supabase, (organizationId) => this.forOrganization(organizationId).audit);
|
|
84
88
|
// The outbox spans organizations — the row it is about carries its own — so it takes a
|
|
85
89
|
// factory and scopes per write rather than being built inside one organization's scope.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@managemint-solutions/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Typed Supabase data-access SDK for ManageMint Solutions",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Scott Bebington <scottbebington@gmail.com>",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"./nest": {
|
|
21
21
|
"types": "./dist/nest/index.d.ts",
|
|
22
22
|
"default": "./dist/nest/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./cache": {
|
|
25
|
+
"types": "./dist/cache/index.d.ts",
|
|
26
|
+
"default": "./dist/cache/index.js"
|
|
23
27
|
}
|
|
24
28
|
},
|
|
25
29
|
"files": [
|
|
@@ -47,11 +51,12 @@
|
|
|
47
51
|
"testEnvironment": "node"
|
|
48
52
|
},
|
|
49
53
|
"dependencies": {
|
|
50
|
-
"@managemint-solutions/entities": "^1.
|
|
54
|
+
"@managemint-solutions/entities": "^1.17.0"
|
|
51
55
|
},
|
|
52
56
|
"peerDependencies": {
|
|
53
57
|
"@nestjs/common": "^11.0.0",
|
|
54
58
|
"@supabase/supabase-js": "^2.103.0",
|
|
59
|
+
"@upstash/redis": "^1.38.0",
|
|
55
60
|
"class-transformer": "^0.5.1",
|
|
56
61
|
"class-validator": "^0.15.0",
|
|
57
62
|
"reflect-metadata": "^0.2.0"
|
|
@@ -59,6 +64,9 @@
|
|
|
59
64
|
"peerDependenciesMeta": {
|
|
60
65
|
"@nestjs/common": {
|
|
61
66
|
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"@upstash/redis": {
|
|
69
|
+
"optional": true
|
|
62
70
|
}
|
|
63
71
|
},
|
|
64
72
|
"devDependencies": {
|
|
@@ -67,6 +75,7 @@
|
|
|
67
75
|
"@types/express": "^5.0.0",
|
|
68
76
|
"@types/jest": "^30.0.0",
|
|
69
77
|
"@types/node": "^22.10.7",
|
|
78
|
+
"@upstash/redis": "^1.38.4",
|
|
70
79
|
"class-transformer": "^0.5.1",
|
|
71
80
|
"class-validator": "^0.15.1",
|
|
72
81
|
"jest": "^30.0.0",
|