@managemint-solutions/sdk 0.1.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 ADDED
@@ -0,0 +1,88 @@
1
+ # ManageMint Solutions SDK
2
+
3
+ Typed Supabase data-access SDK for ManageMint Solutions. Query logic and the generated
4
+ database types live here; consumers (api, portal) get typed responses with no casts.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install @managemint-solutions/sdk @supabase/supabase-js
10
+ ```
11
+
12
+ `@supabase/supabase-js` (^2.103.0), `class-validator` (^0.15) and `class-transformer` (^0.5)
13
+ are peer dependencies — the request DTOs (`AddStatusDto`, `UpdateStatusDto`, `StatusIdDto`,
14
+ `GetStatusesDto`) ship with their class-validator decorators so consumers validate against
15
+ the same rules the SDK's input types describe. `@nestjs/common` (^11) is an optional peer
16
+ dependency, needed only for the `@managemint-solutions/sdk/nest` subpath.
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import { createSupabaseClient, isPostgrestError } from '@managemint-solutions/sdk';
22
+ import type { StatusEntity } from '@managemint-solutions/entities/status';
23
+ import { StatusEntityTypes } from '@managemint-solutions/entities/status/enum';
24
+
25
+ const supabase = createSupabaseClient({
26
+ url: process.env.SUPABASE_URL!,
27
+ key: process.env.SUPABASE_PUBLISHABLE_KEY!, // queries run under the caller's RLS
28
+ accessToken: userJwt,
29
+ });
30
+
31
+ // Inputs and outputs are the shared types from @managemint-solutions/entities.
32
+ const statuses: StatusEntity[] = await supabase.statuses.list(StatusEntityTypes.PROJECT);
33
+ const created = await supabase.statuses.create({
34
+ label: 'In progress',
35
+ colour: '#2ecc71',
36
+ entity: StatusEntityTypes.PROJECT,
37
+ });
38
+ await supabase.statuses.update(created.mms_id, { label: 'Doing', colour: '#2ecc71' });
39
+ await supabase.statuses.remove(created.mms_id);
40
+ ```
41
+
42
+ The client decodes `{ mms_id, organization_id }` from the JWT itself and scopes every query
43
+ by `organization_id`. `supabase.supabase` is the raw typed `@supabase/supabase-js` client
44
+ (`TypedSupabaseClient`) — the escape hatch for queries not yet covered by a resource.
45
+
46
+ ## Errors
47
+
48
+ Every error the SDK throws is a `SupabaseClientError` carrying the HTTP `status` to respond
49
+ with and the client-facing body, `toResponse()` → `{ error, message }`. Postgrest/Postgres
50
+ errors are translated by `mapPostgrestError` (Postgrest code → status, RLS permission and
51
+ module denials → readable messages); JWT problems are `401 Authentication Error`. Consumers
52
+ return these as-is instead of mapping database errors themselves.
53
+
54
+ ## NestJS
55
+
56
+ ```ts
57
+ import { SupabaseUserClient } from '@managemint-solutions/sdk/nest';
58
+ import { GetStatusesDto, SupabaseClient } from '@managemint-solutions/sdk';
59
+
60
+ @Controller('statuses')
61
+ export class StatusesController {
62
+ @Get(':entity')
63
+ getStatuses(@SupabaseUserClient() supabase: SupabaseClient, @Param() params: GetStatusesDto) {
64
+ return supabase.statuses.list(params.entity);
65
+ }
66
+ }
67
+ ```
68
+
69
+ The decorator builds a per-request client from the request's `Bearer` token plus the
70
+ `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` environment variables. A missing or
71
+ malformed token becomes an `UnauthorizedException`. The DTOs work with Nest's global
72
+ `ValidationPipe` as-is (`whitelist`, `forbidNonWhitelisted`, `transform`).
73
+
74
+ `toHttpException(error)` converts a `SupabaseClientError` into an `HttpException` with the
75
+ same status and body — call it from your global exception filter so services can let SDK
76
+ errors propagate without any try/catch.
77
+
78
+ ## Development
79
+
80
+ ```bash
81
+ npm install
82
+ npm test # jest + ts-jest; specs live in src/<resource>/tests/*.spec.ts
83
+ npm run build # tsc -p tsconfig.build.json (excludes specs), CommonJS output in dist/ — the type gate
84
+ ```
85
+
86
+ `src/database.types.ts` is generated by `supabase gen types typescript` and is committed
87
+ here; regenerating it produces a reviewable diff and requires a version bump.
88
+ Publishing happens on push to `main` via `.github/workflows/release.yml`.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export type AuthContext = {
2
+ mms_id: string;
3
+ organization_id: string;
4
+ };
5
+ export declare const decodeAuthContext: (accessToken: string) => AuthContext;
package/dist/auth.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decodeAuthContext = void 0;
4
+ const errors_1 = require("./errors");
5
+ const authError = (message) => new errors_1.SupabaseClientError({ status: 401, error: 'Authentication Error', message });
6
+ const decodeBase64Url = (segment) => typeof Buffer !== 'undefined'
7
+ ? Buffer.from(segment, 'base64url').toString('utf8')
8
+ : atob(segment.replace(/-/g, '+').replace(/_/g, '/')); // browser (portal later)
9
+ const decodeAuthContext = (accessToken) => {
10
+ const parts = accessToken.split('.');
11
+ if (parts.length !== 3)
12
+ throw authError('Malformed JWT');
13
+ let payload;
14
+ try {
15
+ payload = JSON.parse(decodeBase64Url(parts[1]));
16
+ }
17
+ catch {
18
+ throw authError('Invalid JWT payload');
19
+ }
20
+ const mms_id = payload.sub;
21
+ const organization_id = payload.user_metadata?.organization_id;
22
+ if (typeof mms_id !== 'string' ||
23
+ typeof organization_id !== 'string' ||
24
+ organization_id.trim() === '') {
25
+ throw authError('JWT is missing mms_id/organization_id claims');
26
+ }
27
+ return { mms_id, organization_id };
28
+ };
29
+ exports.decodeAuthContext = decodeAuthContext;
30
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":";;;AAAA,qCAA+C;AAI/C,MAAM,SAAS,GAAG,CAAC,OAAe,EAAuB,EAAE,CACzD,IAAI,4BAAmB,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,sBAAsB,EAAE,OAAO,EAAE,CAAC,CAAC;AAEnF,MAAM,eAAe,GAAG,CAAC,OAAe,EAAU,EAAE,CAClD,OAAO,MAAM,KAAK,WAAW;IAC3B,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;IACpD,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,yBAAyB;AAE7E,MAAM,iBAAiB,GAAG,CAAC,WAAmB,EAAe,EAAE;IACpE,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,SAAS,CAAC,eAAe,CAAC,CAAC;IAEzD,IAAI,OAAyE,CAAC;IAC9E,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,SAAS,CAAC,qBAAqB,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAC3B,MAAM,eAAe,GAAG,OAAO,CAAC,aAAa,EAAE,eAAe,CAAC;IAC/D,IACE,OAAO,MAAM,KAAK,QAAQ;QAC1B,OAAO,eAAe,KAAK,QAAQ;QACnC,eAAe,CAAC,IAAI,EAAE,KAAK,EAAE,EAC7B,CAAC;QACD,MAAM,SAAS,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;AACrC,CAAC,CAAC;AAtBW,QAAA,iBAAiB,qBAsB5B"}
@@ -0,0 +1,17 @@
1
+ import { type SupabaseClient as SupabaseJsClient } from '@supabase/supabase-js';
2
+ import type { Database } from './database.types';
3
+ import { type AuthContext } from './auth';
4
+ import { StatusesResource } from './statuses';
5
+ export type TypedSupabaseClient = SupabaseJsClient<Database>;
6
+ export type SupabaseClientConfig = {
7
+ url: string;
8
+ key: string;
9
+ accessToken: string;
10
+ };
11
+ export declare class SupabaseClient {
12
+ readonly supabase: TypedSupabaseClient;
13
+ readonly auth: AuthContext;
14
+ readonly statuses: StatusesResource;
15
+ constructor(config: SupabaseClientConfig);
16
+ }
17
+ export declare const createSupabaseClient: (config: SupabaseClientConfig) => SupabaseClient;
package/dist/client.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSupabaseClient = exports.SupabaseClient = void 0;
4
+ const supabase_js_1 = require("@supabase/supabase-js");
5
+ const auth_1 = require("./auth");
6
+ const statuses_1 = require("./statuses");
7
+ class SupabaseClient {
8
+ supabase; // escape hatch for not-yet-migrated queries
9
+ auth;
10
+ statuses;
11
+ constructor(config) {
12
+ this.auth = (0, auth_1.decodeAuthContext)(config.accessToken);
13
+ this.supabase = (0, supabase_js_1.createClient)(config.url, config.key, {
14
+ global: { headers: { Authorization: `Bearer ${config.accessToken}` } },
15
+ auth: { persistSession: false, autoRefreshToken: false },
16
+ });
17
+ this.statuses = new statuses_1.StatusesResource(this.supabase, this.auth);
18
+ }
19
+ }
20
+ exports.SupabaseClient = SupabaseClient;
21
+ const createSupabaseClient = (config) => new SupabaseClient(config);
22
+ exports.createSupabaseClient = createSupabaseClient;
23
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;AAAA,uDAA8F;AAE9F,iCAA6D;AAC7D,yCAA8C;AAU9C,MAAa,cAAc;IAChB,QAAQ,CAAsB,CAAC,4CAA4C;IAC3E,IAAI,CAAc;IAClB,QAAQ,CAAmB;IAEpC,YAAY,MAA4B;QACtC,IAAI,CAAC,IAAI,GAAG,IAAA,wBAAiB,EAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QAClD,IAAI,CAAC,QAAQ,GAAG,IAAA,0BAAY,EAAW,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE;YAC7D,MAAM,EAAE,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,CAAC,WAAW,EAAE,EAAE,EAAE;YACtE,IAAI,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE;SACzD,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,GAAG,IAAI,2BAAgB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACjE,CAAC;CACF;AAbD,wCAaC;AAEM,MAAM,oBAAoB,GAAG,CAAC,MAA4B,EAAkB,EAAE,CACnF,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC;AADhB,QAAA,oBAAoB,wBACJ"}