@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 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.SupportingFileIdDto = exports.GetSupportingFilesDto = 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.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 = 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.33.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.16.0"
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",