@selvajs/supabase-provider 0.11.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/LICENSE +21 -0
- package/README.md +271 -0
- package/dist/auth/SupabaseAuthProvider.d.ts +81 -0
- package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -0
- package/dist/auth/SupabaseAuthProvider.js +383 -0
- package/dist/auth/SupabaseAuthProvider.js.map +1 -0
- package/dist/auth/__tests__/conformance.test.d.ts +2 -0
- package/dist/auth/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/auth/__tests__/conformance.test.js +76 -0
- package/dist/auth/__tests__/conformance.test.js.map +1 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/data/SupabaseAuditQuery.d.ts +16 -0
- package/dist/data/SupabaseAuditQuery.d.ts.map +1 -0
- package/dist/data/SupabaseAuditQuery.js +83 -0
- package/dist/data/SupabaseAuditQuery.js.map +1 -0
- package/dist/data/SupabaseComputeServerStore.d.ts +26 -0
- package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -0
- package/dist/data/SupabaseComputeServerStore.js +227 -0
- package/dist/data/SupabaseComputeServerStore.js.map +1 -0
- package/dist/data/SupabaseDataProvider.d.ts +55 -0
- package/dist/data/SupabaseDataProvider.d.ts.map +1 -0
- package/dist/data/SupabaseDataProvider.js +114 -0
- package/dist/data/SupabaseDataProvider.js.map +1 -0
- package/dist/data/SupabaseDefinitionStore.d.ts +39 -0
- package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -0
- package/dist/data/SupabaseDefinitionStore.js +419 -0
- package/dist/data/SupabaseDefinitionStore.js.map +1 -0
- package/dist/data/SupabaseEventSink.d.ts +17 -0
- package/dist/data/SupabaseEventSink.d.ts.map +1 -0
- package/dist/data/SupabaseEventSink.js +31 -0
- package/dist/data/SupabaseEventSink.js.map +1 -0
- package/dist/data/SupabaseInviteStore.d.ts +22 -0
- package/dist/data/SupabaseInviteStore.d.ts.map +1 -0
- package/dist/data/SupabaseInviteStore.js +159 -0
- package/dist/data/SupabaseInviteStore.js.map +1 -0
- package/dist/data/SupabaseOrgStore.d.ts +33 -0
- package/dist/data/SupabaseOrgStore.d.ts.map +1 -0
- package/dist/data/SupabaseOrgStore.js +455 -0
- package/dist/data/SupabaseOrgStore.js.map +1 -0
- package/dist/data/SupabaseProjectStore.d.ts +29 -0
- package/dist/data/SupabaseProjectStore.d.ts.map +1 -0
- package/dist/data/SupabaseProjectStore.js +328 -0
- package/dist/data/SupabaseProjectStore.js.map +1 -0
- package/dist/data/SupabaseShareLinkStore.d.ts +26 -0
- package/dist/data/SupabaseShareLinkStore.d.ts.map +1 -0
- package/dist/data/SupabaseShareLinkStore.js +150 -0
- package/dist/data/SupabaseShareLinkStore.js.map +1 -0
- package/dist/data/__tests__/compute-server-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/compute-server-conformance.test.js +40 -0
- package/dist/data/__tests__/compute-server-conformance.test.js.map +1 -0
- package/dist/data/__tests__/definition-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/definition-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/definition-conformance.test.js +85 -0
- package/dist/data/__tests__/definition-conformance.test.js.map +1 -0
- package/dist/data/__tests__/event-sink-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/event-sink-conformance.test.js +27 -0
- package/dist/data/__tests__/event-sink-conformance.test.js.map +1 -0
- package/dist/data/__tests__/invite-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/invite-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/invite-conformance.test.js +45 -0
- package/dist/data/__tests__/invite-conformance.test.js.map +1 -0
- package/dist/data/__tests__/org-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/org-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/org-conformance.test.js +29 -0
- package/dist/data/__tests__/org-conformance.test.js.map +1 -0
- package/dist/data/__tests__/project-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/project-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/project-conformance.test.js +57 -0
- package/dist/data/__tests__/project-conformance.test.js.map +1 -0
- package/dist/data/__tests__/share-link-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/share-link-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/share-link-conformance.test.js +88 -0
- package/dist/data/__tests__/share-link-conformance.test.js.map +1 -0
- package/dist/data/__tests__/test-helpers.d.ts +53 -0
- package/dist/data/__tests__/test-helpers.d.ts.map +1 -0
- package/dist/data/__tests__/test-helpers.js +175 -0
- package/dist/data/__tests__/test-helpers.js.map +1 -0
- package/dist/data/client.d.ts +44 -0
- package/dist/data/client.d.ts.map +1 -0
- package/dist/data/client.js +40 -0
- package/dist/data/client.js.map +1 -0
- package/dist/data/index.d.ts +12 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +11 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/pagination.d.ts +30 -0
- package/dist/data/pagination.d.ts.map +1 -0
- package/dist/data/pagination.js +53 -0
- package/dist/data/pagination.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/permissions/SupabasePlatformPermissionStore.d.ts +25 -0
- package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -0
- package/dist/permissions/SupabasePlatformPermissionStore.js +153 -0
- package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -0
- package/dist/permissions/__tests__/conformance.test.d.ts +2 -0
- package/dist/permissions/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/permissions/__tests__/conformance.test.js +27 -0
- package/dist/permissions/__tests__/conformance.test.js.map +1 -0
- package/dist/permissions/index.d.ts +2 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +2 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/storage/SupabaseStorageProvider.d.ts +67 -0
- package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -0
- package/dist/storage/SupabaseStorageProvider.js +136 -0
- package/dist/storage/SupabaseStorageProvider.js.map +1 -0
- package/dist/storage/__tests__/conformance.test.d.ts +2 -0
- package/dist/storage/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/storage/__tests__/conformance.test.js +149 -0
- package/dist/storage/__tests__/conformance.test.js.map +1 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/userProfile/SupabaseUserProfileProvider.d.ts +16 -0
- package/dist/userProfile/SupabaseUserProfileProvider.d.ts.map +1 -0
- package/dist/userProfile/SupabaseUserProfileProvider.js +148 -0
- package/dist/userProfile/SupabaseUserProfileProvider.js.map +1 -0
- package/dist/userProfile/__tests__/conformance.test.d.ts +2 -0
- package/dist/userProfile/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/userProfile/__tests__/conformance.test.js +38 -0
- package/dist/userProfile/__tests__/conformance.test.js.map +1 -0
- package/dist/userProfile/index.d.ts +2 -0
- package/dist/userProfile/index.d.ts.map +1 -0
- package/dist/userProfile/index.js +2 -0
- package/dist/userProfile/index.js.map +1 -0
- package/package.json +75 -0
- package/src/auth/SupabaseAuthProvider.ts +464 -0
- package/src/auth/__tests__/conformance.test.ts +78 -0
- package/src/auth/index.ts +2 -0
- package/src/data/SupabaseAuditQuery.ts +103 -0
- package/src/data/SupabaseComputeServerStore.ts +262 -0
- package/src/data/SupabaseDataProvider.ts +125 -0
- package/src/data/SupabaseDefinitionStore.ts +504 -0
- package/src/data/SupabaseEventSink.ts +30 -0
- package/src/data/SupabaseInviteStore.ts +183 -0
- package/src/data/SupabaseOrgStore.ts +547 -0
- package/src/data/SupabaseProjectStore.ts +414 -0
- package/src/data/SupabaseShareLinkStore.ts +191 -0
- package/src/data/__tests__/compute-server-conformance.test.ts +40 -0
- package/src/data/__tests__/definition-conformance.test.ts +89 -0
- package/src/data/__tests__/event-sink-conformance.test.ts +32 -0
- package/src/data/__tests__/invite-conformance.test.ts +46 -0
- package/src/data/__tests__/org-conformance.test.ts +30 -0
- package/src/data/__tests__/project-conformance.test.ts +57 -0
- package/src/data/__tests__/share-link-conformance.test.ts +87 -0
- package/src/data/__tests__/test-helpers.ts +186 -0
- package/src/data/client.ts +86 -0
- package/src/data/index.ts +11 -0
- package/src/data/pagination.ts +68 -0
- package/src/index.ts +23 -0
- package/src/permissions/SupabasePlatformPermissionStore.ts +161 -0
- package/src/permissions/__tests__/conformance.test.ts +28 -0
- package/src/permissions/index.ts +1 -0
- package/src/storage/SupabaseStorageProvider.ts +177 -0
- package/src/storage/__tests__/conformance.test.ts +152 -0
- package/src/storage/index.ts +2 -0
- package/src/userProfile/SupabaseUserProfileProvider.ts +177 -0
- package/src/userProfile/__tests__/conformance.test.ts +39 -0
- package/src/userProfile/index.ts +1 -0
- package/supabase/.branches/_current_branch +1 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/config.toml +68 -0
- package/supabase/migrations/0001_initial.sql +1084 -0
- package/supabase/seed.sql +13 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, beforeEach, it } from 'vitest';
|
|
2
|
+
import { runPlatformPermissionStoreConformance } from '@selvajs/platform/testing';
|
|
3
|
+
import { SupabasePlatformPermissionStore } from '../SupabasePlatformPermissionStore.js';
|
|
4
|
+
import { readEnv, resetAllData, seedPlainUser } from '../../data/__tests__/test-helpers.js';
|
|
5
|
+
const envCtx = readEnv();
|
|
6
|
+
if (!envCtx) {
|
|
7
|
+
describe.skip('SupabasePlatformPermissionStore (skipped: no live stack)', () => {
|
|
8
|
+
it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => { });
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
describe('SupabasePlatformPermissionStore', () => {
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
await resetAllData(envCtx);
|
|
15
|
+
});
|
|
16
|
+
runPlatformPermissionStoreConformance({
|
|
17
|
+
name: 'SupabasePlatformPermissionStore',
|
|
18
|
+
createStore: async () => {
|
|
19
|
+
return {
|
|
20
|
+
store: new SupabasePlatformPermissionStore(envCtx.bundle),
|
|
21
|
+
seedUser: async () => (await seedPlainUser(envCtx, '')).userId
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=conformance.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.test.js","sourceRoot":"","sources":["../../../src/permissions/__tests__/conformance.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAE,qCAAqC,EAAE,MAAM,2BAA2B,CAAC;AAClF,OAAO,EAAE,+BAA+B,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAE5F,MAAM,MAAM,GAAG,OAAO,EAAE,CAAC;AAEzB,IAAI,CAAC,MAAM,EAAE,CAAC;IACb,QAAQ,CAAC,IAAI,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAC9E,EAAE,CAAC,uFAAuF,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACvG,CAAC,CAAC,CAAC;AACJ,CAAC;KAAM,CAAC;IACP,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;QAChD,UAAU,CAAC,KAAK,IAAI,EAAE;YACrB,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;QAEH,qCAAqC,CAAC;YACrC,IAAI,EAAE,iCAAiC;YACvC,WAAW,EAAE,KAAK,IAAI,EAAE;gBACvB,OAAO;oBACN,KAAK,EAAE,IAAI,+BAA+B,CAAC,MAAM,CAAC,MAAM,CAAC;oBACzD,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM;iBAC9D,CAAC;YACH,CAAC;SACD,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/permissions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,+BAA+B,EAAE,MAAM,sCAAsC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/permissions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,+BAA+B,EAAE,MAAM,sCAAsC,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { IStorageProvider } from '@selvajs/platform/storage';
|
|
2
|
+
/**
|
|
3
|
+
* Storage backend for Selva on Supabase Storage.
|
|
4
|
+
*
|
|
5
|
+
* Uses two buckets:
|
|
6
|
+
* - {publicBucket} (public): cover images, archive thumbnails, anything
|
|
7
|
+
* CDN-safe. `getPublicUrl` returns the direct CDN URL.
|
|
8
|
+
* - {privateBucket} (private): .gh / .ghx source files. `getPublicUrl`
|
|
9
|
+
* returns the authenticated proxy URL (`${privateUrlPrefix}/{path}`) —
|
|
10
|
+
* the consuming app must have a route at that prefix that authenticates
|
|
11
|
+
* the request and streams the bytes via `get()`.
|
|
12
|
+
*
|
|
13
|
+
* Path routing: any path whose basename matches `definition.{ext}` is
|
|
14
|
+
* considered private. Covers (`cover.webp`) and archives (`archive/{ref}`)
|
|
15
|
+
* stay public. `definitionPaths` from @selvajs/platform produces exactly these
|
|
16
|
+
* names, so the routing is implicit — no caller has to think about buckets.
|
|
17
|
+
*/
|
|
18
|
+
export interface SupabaseStorageProviderConfig {
|
|
19
|
+
/** Supabase project URL (e.g. http://127.0.0.1:54321 for local stack). */
|
|
20
|
+
supabaseUrl: string;
|
|
21
|
+
/**
|
|
22
|
+
* Service-role key. Required because the provider runs server-side and
|
|
23
|
+
* must bypass RLS for server-internal actions (admin uploads, janitor
|
|
24
|
+
* cleanup). The provider is never used directly from the browser.
|
|
25
|
+
*/
|
|
26
|
+
serviceRoleKey: string;
|
|
27
|
+
/** Bucket for CDN-readable objects. Default: "selva-public". */
|
|
28
|
+
publicBucket?: string;
|
|
29
|
+
/** Bucket for authenticated-only objects. Default: "selva-private". */
|
|
30
|
+
privateBucket?: string;
|
|
31
|
+
/**
|
|
32
|
+
* URL prefix the app exposes for authenticated file downloads. Used by
|
|
33
|
+
* `getPublicUrl` for private-bucket paths. Default: "/api/files".
|
|
34
|
+
* The matching SvelteKit route is expected to call `get()` and stream
|
|
35
|
+
* bytes back after an auth check.
|
|
36
|
+
*/
|
|
37
|
+
privateUrlPrefix?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare class SupabaseStorageProvider implements IStorageProvider {
|
|
40
|
+
private readonly client;
|
|
41
|
+
private readonly publicBucket;
|
|
42
|
+
private readonly privateBucket;
|
|
43
|
+
private readonly publicBaseUrl;
|
|
44
|
+
private readonly privateUrlPrefix;
|
|
45
|
+
static fromEnv(env: Record<string, string | undefined>): SupabaseStorageProvider;
|
|
46
|
+
constructor(config: SupabaseStorageProviderConfig);
|
|
47
|
+
/**
|
|
48
|
+
* Route a storage path to a bucket. Any `.gh`/`.ghx` source file is
|
|
49
|
+
* private — confidentiality is determined by extension, not location, so
|
|
50
|
+
* a path-scheme rename (e.g. `definition.gh` → `versions/v1.gh`) can't
|
|
51
|
+
* silently move source files into the public bucket. Everything else
|
|
52
|
+
* (covers, archives, thumbnails) is public.
|
|
53
|
+
*/
|
|
54
|
+
private bucketFor;
|
|
55
|
+
get(storagePath: string): Promise<Uint8Array | null>;
|
|
56
|
+
put(storagePath: string, data: Uint8Array, contentType?: string): Promise<void>;
|
|
57
|
+
delete(storagePath: string): Promise<void>;
|
|
58
|
+
deletePrefix(prefix: string): Promise<void>;
|
|
59
|
+
private deletePrefixInBucket;
|
|
60
|
+
/**
|
|
61
|
+
* Recursively list every key under a prefix. `storage-js`'s `list` API
|
|
62
|
+
* only returns the immediate children of a "folder", so we DFS.
|
|
63
|
+
*/
|
|
64
|
+
private listAllKeys;
|
|
65
|
+
getPublicUrl(storagePath: string): string;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=SupabaseStorageProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SupabaseStorageProvider.d.ts","sourceRoot":"","sources":["../../src/storage/SupabaseStorageProvider.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,6BAA6B;IAC7C,0EAA0E;IAC1E,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,uBAAwB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,uBAAuB;gBAcpE,MAAM,EAAE,6BAA6B;IAWjD;;;;;;OAMG;IACH,OAAO,CAAC,SAAS;IAIX,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAcpD,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc/E,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO1C,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YASnC,oBAAoB;IAYlC;;;OAGG;YACW,WAAW;IAuBzB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;CAOzC"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import { transcodeImageIfNeeded } from '@selvajs/platform/storage';
|
|
3
|
+
export class SupabaseStorageProvider {
|
|
4
|
+
client;
|
|
5
|
+
publicBucket;
|
|
6
|
+
privateBucket;
|
|
7
|
+
publicBaseUrl;
|
|
8
|
+
privateUrlPrefix;
|
|
9
|
+
static fromEnv(env) {
|
|
10
|
+
const supabaseUrl = env.SUPABASE_URL;
|
|
11
|
+
const serviceRoleKey = env.SUPABASE_SERVICE_ROLE_KEY;
|
|
12
|
+
if (!supabaseUrl)
|
|
13
|
+
throw new Error('Missing required env var: SUPABASE_URL');
|
|
14
|
+
if (!serviceRoleKey)
|
|
15
|
+
throw new Error('Missing required env var: SUPABASE_SERVICE_ROLE_KEY');
|
|
16
|
+
return new SupabaseStorageProvider({
|
|
17
|
+
supabaseUrl,
|
|
18
|
+
serviceRoleKey,
|
|
19
|
+
publicBucket: env.SUPABASE_PUBLIC_BUCKET,
|
|
20
|
+
privateBucket: env.SUPABASE_PRIVATE_BUCKET,
|
|
21
|
+
privateUrlPrefix: env.SUPABASE_PRIVATE_URL_PREFIX
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.client = createClient(config.supabaseUrl, config.serviceRoleKey, {
|
|
26
|
+
auth: { persistSession: false, autoRefreshToken: false }
|
|
27
|
+
});
|
|
28
|
+
this.publicBucket = config.publicBucket ?? 'selva-public';
|
|
29
|
+
this.privateBucket = config.privateBucket ?? 'selva-private';
|
|
30
|
+
// Direct CDN URL format: {supabaseUrl}/storage/v1/object/public/{bucket}/{path}
|
|
31
|
+
this.publicBaseUrl = `${config.supabaseUrl.replace(/\/$/, '')}/storage/v1/object/public/${this.publicBucket}`;
|
|
32
|
+
this.privateUrlPrefix = (config.privateUrlPrefix ?? '/api/files').replace(/\/$/, '');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Route a storage path to a bucket. Any `.gh`/`.ghx` source file is
|
|
36
|
+
* private — confidentiality is determined by extension, not location, so
|
|
37
|
+
* a path-scheme rename (e.g. `definition.gh` → `versions/v1.gh`) can't
|
|
38
|
+
* silently move source files into the public bucket. Everything else
|
|
39
|
+
* (covers, archives, thumbnails) is public.
|
|
40
|
+
*/
|
|
41
|
+
bucketFor(storagePath) {
|
|
42
|
+
return /\.(gh|ghx)$/i.test(storagePath) ? this.privateBucket : this.publicBucket;
|
|
43
|
+
}
|
|
44
|
+
async get(storagePath) {
|
|
45
|
+
const bucket = this.bucketFor(storagePath);
|
|
46
|
+
const { data, error } = await this.client.storage.from(bucket).download(storagePath);
|
|
47
|
+
if (error) {
|
|
48
|
+
// storage-js surfaces 404 as an error with message "Object not found".
|
|
49
|
+
// Normalize any "not found" to null — we don't have a stable status code.
|
|
50
|
+
const msg = error.message ?? '';
|
|
51
|
+
if (/not found/i.test(msg) || /no such key/i.test(msg))
|
|
52
|
+
return null;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
const buffer = await data.arrayBuffer();
|
|
56
|
+
return new Uint8Array(buffer);
|
|
57
|
+
}
|
|
58
|
+
async put(storagePath, data, contentType) {
|
|
59
|
+
// Normalize images through the shared helper so this provider produces
|
|
60
|
+
// the same bytes / extensions as LocalStorageProvider for the same input.
|
|
61
|
+
const transcoded = await transcodeImageIfNeeded(data, contentType, storagePath);
|
|
62
|
+
const bucket = this.bucketFor(transcoded.path);
|
|
63
|
+
const { error } = await this.client.storage
|
|
64
|
+
.from(bucket)
|
|
65
|
+
.upload(transcoded.path, transcoded.data, {
|
|
66
|
+
contentType: transcoded.contentType,
|
|
67
|
+
upsert: true
|
|
68
|
+
});
|
|
69
|
+
if (error)
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
async delete(storagePath) {
|
|
73
|
+
const bucket = this.bucketFor(storagePath);
|
|
74
|
+
const { error } = await this.client.storage.from(bucket).remove([storagePath]);
|
|
75
|
+
// remove() does not error for missing paths — it just returns an empty array.
|
|
76
|
+
if (error)
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
async deletePrefix(prefix) {
|
|
80
|
+
// Hit both buckets: a definition's GUID prefix may have files in each.
|
|
81
|
+
// Scoped by the trailing slash in the prefix, so siblings aren't touched.
|
|
82
|
+
await Promise.all([
|
|
83
|
+
this.deletePrefixInBucket(this.publicBucket, prefix),
|
|
84
|
+
this.deletePrefixInBucket(this.privateBucket, prefix)
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
async deletePrefixInBucket(bucket, prefix) {
|
|
88
|
+
const normalized = prefix.replace(/\/+$/, '');
|
|
89
|
+
const keys = await this.listAllKeys(bucket, normalized);
|
|
90
|
+
if (keys.length === 0)
|
|
91
|
+
return;
|
|
92
|
+
// `remove` accepts up to 1000 paths; batch just in case.
|
|
93
|
+
for (let i = 0; i < keys.length; i += 1000) {
|
|
94
|
+
const slice = keys.slice(i, i + 1000);
|
|
95
|
+
const { error } = await this.client.storage.from(bucket).remove(slice);
|
|
96
|
+
if (error)
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Recursively list every key under a prefix. `storage-js`'s `list` API
|
|
102
|
+
* only returns the immediate children of a "folder", so we DFS.
|
|
103
|
+
*/
|
|
104
|
+
async listAllKeys(bucket, prefix) {
|
|
105
|
+
const out = [];
|
|
106
|
+
const stack = [prefix];
|
|
107
|
+
while (stack.length > 0) {
|
|
108
|
+
const dir = stack.pop();
|
|
109
|
+
const { data, error } = await this.client.storage.from(bucket).list(dir, {
|
|
110
|
+
limit: 1000,
|
|
111
|
+
sortBy: { column: 'name', order: 'asc' }
|
|
112
|
+
});
|
|
113
|
+
if (error)
|
|
114
|
+
throw error;
|
|
115
|
+
for (const entry of data ?? []) {
|
|
116
|
+
const full = dir ? `${dir}/${entry.name}` : entry.name;
|
|
117
|
+
// Folder entries have `id === null` in storage-js.
|
|
118
|
+
if (entry.id === null) {
|
|
119
|
+
stack.push(full);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
out.push(full);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
getPublicUrl(storagePath) {
|
|
129
|
+
if (this.bucketFor(storagePath) === this.publicBucket) {
|
|
130
|
+
return `${this.publicBaseUrl}/${storagePath}`;
|
|
131
|
+
}
|
|
132
|
+
// Private files: the app's authenticated proxy route. See `privateUrlPrefix`.
|
|
133
|
+
return `${this.privateUrlPrefix}/${storagePath}`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=SupabaseStorageProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SupabaseStorageProvider.js","sourceRoot":"","sources":["../../src/storage/SupabaseStorageProvider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.test.d.ts","sourceRoot":"","sources":["../../../src/storage/__tests__/conformance.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, beforeEach, it, expect } from 'vitest';
|
|
2
|
+
import { createClient } from '@supabase/supabase-js';
|
|
3
|
+
import { runStorageProviderConformance } from '@selvajs/platform/testing';
|
|
4
|
+
import { definitionPaths } from '@selvajs/platform';
|
|
5
|
+
import { SupabaseStorageProvider } from '../SupabaseStorageProvider.js';
|
|
6
|
+
const SUPABASE_URL = process.env.SUPABASE_URL;
|
|
7
|
+
const SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
8
|
+
const PUBLIC_BUCKET = process.env.SUPABASE_PUBLIC_BUCKET ?? 'selva-public';
|
|
9
|
+
const PRIVATE_BUCKET = process.env.SUPABASE_PRIVATE_BUCKET ?? 'selva-private';
|
|
10
|
+
// The conformance suite hits a live Supabase stack. If creds aren't present
|
|
11
|
+
// (e.g. in CI without Docker, or a clean clone pre-`supabase start`), skip
|
|
12
|
+
// the suite with a single explanatory test so the run surfaces the reason.
|
|
13
|
+
// `createClient` transitively loads `@supabase/realtime-js`, which on Node < 22
|
|
14
|
+
// throws at construction without native WebSocket — treat that as "no stack"
|
|
15
|
+
// rather than crashing the suite during collection.
|
|
16
|
+
function tryBuildAdminClient() {
|
|
17
|
+
if (!SUPABASE_URL || !SERVICE_ROLE_KEY)
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
return createClient(SUPABASE_URL, SERVICE_ROLE_KEY, {
|
|
21
|
+
auth: { persistSession: false, autoRefreshToken: false }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
26
|
+
if (msg.includes('WebSocket'))
|
|
27
|
+
return null;
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const _adminClient = tryBuildAdminClient();
|
|
32
|
+
if (_adminClient === null) {
|
|
33
|
+
describe.skip('SupabaseStorageProvider (skipped: no live stack)', () => {
|
|
34
|
+
it('populate packages/providers/supabase/.env.test with SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY (from `npx supabase start`) to run these tests', () => { });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Re-bind in this branch so closures below see the non-null narrowed type.
|
|
39
|
+
const adminClient = _adminClient;
|
|
40
|
+
/**
|
|
41
|
+
* Delete every object under a prefix in a bucket. The conformance suite
|
|
42
|
+
* assumes fresh state per test; Supabase Storage is shared across runs,
|
|
43
|
+
* so we reset both buckets before each test.
|
|
44
|
+
*/
|
|
45
|
+
async function emptyBucket(bucket) {
|
|
46
|
+
const keys = [];
|
|
47
|
+
const stack = [''];
|
|
48
|
+
while (stack.length > 0) {
|
|
49
|
+
const dir = stack.pop();
|
|
50
|
+
const { data, error } = await adminClient.storage.from(bucket).list(dir, {
|
|
51
|
+
limit: 1000,
|
|
52
|
+
sortBy: { column: 'name', order: 'asc' }
|
|
53
|
+
});
|
|
54
|
+
if (error)
|
|
55
|
+
throw error;
|
|
56
|
+
for (const entry of data ?? []) {
|
|
57
|
+
const full = dir ? `${dir}/${entry.name}` : entry.name;
|
|
58
|
+
if (entry.id === null)
|
|
59
|
+
stack.push(full);
|
|
60
|
+
else
|
|
61
|
+
keys.push(full);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (keys.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
for (let i = 0; i < keys.length; i += 1000) {
|
|
67
|
+
const { error } = await adminClient.storage.from(bucket).remove(keys.slice(i, i + 1000));
|
|
68
|
+
if (error)
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
describe('SupabaseStorageProvider', () => {
|
|
73
|
+
beforeEach(async () => {
|
|
74
|
+
await Promise.all([emptyBucket(PUBLIC_BUCKET), emptyBucket(PRIVATE_BUCKET)]);
|
|
75
|
+
});
|
|
76
|
+
runStorageProviderConformance({
|
|
77
|
+
name: 'SupabaseStorageProvider',
|
|
78
|
+
createStorage: () => new SupabaseStorageProvider({
|
|
79
|
+
supabaseUrl: SUPABASE_URL,
|
|
80
|
+
serviceRoleKey: SERVICE_ROLE_KEY,
|
|
81
|
+
publicBucket: PUBLIC_BUCKET,
|
|
82
|
+
privateBucket: PRIVATE_BUCKET
|
|
83
|
+
})
|
|
84
|
+
});
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Bucket routing — Supabase-specific (the conformance suite is provider-
|
|
87
|
+
// agnostic and can't see which bucket a file lands in). Without these tests
|
|
88
|
+
// a regex regression in `bucketFor` could silently move .gh source files
|
|
89
|
+
// into the public/CDN bucket.
|
|
90
|
+
// ============================================================================
|
|
91
|
+
describe('bucket routing', () => {
|
|
92
|
+
const storage = new SupabaseStorageProvider({
|
|
93
|
+
supabaseUrl: SUPABASE_URL,
|
|
94
|
+
serviceRoleKey: SERVICE_ROLE_KEY,
|
|
95
|
+
publicBucket: PUBLIC_BUCKET,
|
|
96
|
+
privateBucket: PRIVATE_BUCKET
|
|
97
|
+
});
|
|
98
|
+
async function existsInBucket(bucket, path) {
|
|
99
|
+
const { data, error } = await adminClient.storage.from(bucket).download(path);
|
|
100
|
+
if (error)
|
|
101
|
+
return false;
|
|
102
|
+
return Boolean(data);
|
|
103
|
+
}
|
|
104
|
+
it('versioned .gh files land in the private bucket', async () => {
|
|
105
|
+
const guid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
|
106
|
+
const path = definitionPaths.version(guid, 1, 'gh');
|
|
107
|
+
await storage.put(path, new TextEncoder().encode('GH_BYTES'));
|
|
108
|
+
expect(await existsInBucket(PRIVATE_BUCKET, path)).toBe(true);
|
|
109
|
+
expect(await existsInBucket(PUBLIC_BUCKET, path)).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
it('versioned .ghx files land in the private bucket', async () => {
|
|
112
|
+
const guid = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
|
|
113
|
+
const path = definitionPaths.version(guid, 3, 'ghx');
|
|
114
|
+
await storage.put(path, new TextEncoder().encode('GHX_BYTES'));
|
|
115
|
+
expect(await existsInBucket(PRIVATE_BUCKET, path)).toBe(true);
|
|
116
|
+
expect(await existsInBucket(PUBLIC_BUCKET, path)).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
it('cover images land in the public bucket', async () => {
|
|
119
|
+
const guid = 'cccccccc-cccc-cccc-cccc-cccccccccccc';
|
|
120
|
+
const path = definitionPaths.image(guid);
|
|
121
|
+
// Real 1×1 transparent PNG. The provider runs every image through
|
|
122
|
+
// transcodeImageIfNeeded → sharp, so a hand-crafted byte stub
|
|
123
|
+
// would fail the decode. Routing (the test's actual concern) is
|
|
124
|
+
// independent of which format the input arrives in.
|
|
125
|
+
const tinyPng = Uint8Array.from([
|
|
126
|
+
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8,
|
|
127
|
+
6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 3, 232, 0, 0, 3, 232, 1,
|
|
128
|
+
181, 123, 82, 107, 0, 0, 0, 13, 73, 68, 65, 84, 120, 156, 99, 96, 96, 96, 96, 0, 0, 0, 5,
|
|
129
|
+
0, 1, 165, 246, 69, 64, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130
|
|
130
|
+
]);
|
|
131
|
+
await storage.put(path, tinyPng, 'image/png');
|
|
132
|
+
expect(await existsInBucket(PUBLIC_BUCKET, path)).toBe(true);
|
|
133
|
+
expect(await existsInBucket(PRIVATE_BUCKET, path)).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
it('getPublicUrl returns the proxy prefix for private files', () => {
|
|
136
|
+
const path = definitionPaths.version('guid-1', 1, 'gh');
|
|
137
|
+
const url = storage.getPublicUrl(path);
|
|
138
|
+
expect(url.startsWith('/api/files/')).toBe(true);
|
|
139
|
+
expect(url).not.toContain('/storage/v1/object/public/');
|
|
140
|
+
});
|
|
141
|
+
it('getPublicUrl returns the CDN URL for public files', () => {
|
|
142
|
+
const path = definitionPaths.image('guid-1');
|
|
143
|
+
const url = storage.getPublicUrl(path);
|
|
144
|
+
expect(url).toContain(`/storage/v1/object/public/${PUBLIC_BUCKET}/`);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=conformance.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.test.js","sourceRoot":"","sources":["../../../src/storage/__tests__/conformance.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,YAAY,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IUserProfileStore, RecentRun, RequestContext, UserManagementResult, UserProfile } from '@selvajs/platform';
|
|
2
|
+
import type { ClientBundle } from '../data/client.js';
|
|
3
|
+
export declare class SupabaseUserProfileProvider implements IUserProfileStore {
|
|
4
|
+
private readonly clients;
|
|
5
|
+
constructor(clients: ClientBundle);
|
|
6
|
+
private client;
|
|
7
|
+
getProfile(ctx: RequestContext, userId: string): Promise<UserProfile | null>;
|
|
8
|
+
getProfiles(_ctx: RequestContext, userIds: readonly string[]): Promise<UserProfile[]>;
|
|
9
|
+
updateProfile(ctx: RequestContext, userId: string, patch: {
|
|
10
|
+
displayName?: string;
|
|
11
|
+
}): Promise<UserManagementResult>;
|
|
12
|
+
starDefinition(ctx: RequestContext, userId: string, definitionId: string): Promise<UserManagementResult>;
|
|
13
|
+
unstarDefinition(ctx: RequestContext, userId: string, definitionId: string): Promise<UserManagementResult>;
|
|
14
|
+
recordRun(ctx: RequestContext, userId: string, run: RecentRun): Promise<UserManagementResult>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=SupabaseUserProfileProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SupabaseUserProfileProvider.d.ts","sourceRoot":"","sources":["../../src/userProfile/SupabaseUserProfileProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,iBAAiB,EACjB,SAAS,EACT,cAAc,EACd,oBAAoB,EACpB,WAAW,EACX,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAetD,qBAAa,2BAA4B,YAAW,iBAAiB;IACxD,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,YAAY;IAElD,OAAO,CAAC,MAAM;IAIR,UAAU,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAW5E,WAAW,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAYrF,aAAa,CAClB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAC7B,OAAO,CAAC,oBAAoB,CAAC;IAe1B,cAAc,CACnB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,CAAC;IAmB1B,gBAAgB,CACrB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,CAAC;IAgB1B,SAAS,CACd,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,GACZ,OAAO,CAAC,oBAAoB,CAAC;CAiBhC"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { ProviderError, hasPermission } from '@selvajs/platform';
|
|
2
|
+
/**
|
|
3
|
+
* User profile backed by `public.user_profiles` (one row per `auth.users.id`).
|
|
4
|
+
* The row is auto-seeded by the `handle_new_auth_user` trigger on signup, so
|
|
5
|
+
* the store never inserts — only reads/updates.
|
|
6
|
+
*
|
|
7
|
+
* Uses the service-role client throughout (RLS on `user_profiles` is
|
|
8
|
+
* coarse-grained — server-side flows like the hooks.server.ts profile load
|
|
9
|
+
* need access regardless of the caller's identity). The `assertCanAccess`
|
|
10
|
+
* check enforces "you can only see/modify your own profile" at the app layer
|
|
11
|
+
* to match the `IUserProfileStore` contract.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_RECENT_RUNS = 20;
|
|
14
|
+
export class SupabaseUserProfileProvider {
|
|
15
|
+
clients;
|
|
16
|
+
constructor(clients) {
|
|
17
|
+
this.clients = clients;
|
|
18
|
+
}
|
|
19
|
+
client() {
|
|
20
|
+
return this.clients.serviceClient;
|
|
21
|
+
}
|
|
22
|
+
async getProfile(ctx, userId) {
|
|
23
|
+
assertCanAccess(ctx, userId);
|
|
24
|
+
const { data, error } = await this.client()
|
|
25
|
+
.from('user_profiles')
|
|
26
|
+
.select('user_id, display_name, starred_definitions, recent_runs')
|
|
27
|
+
.eq('user_id', userId)
|
|
28
|
+
.maybeSingle();
|
|
29
|
+
if (error)
|
|
30
|
+
throw mapError(error);
|
|
31
|
+
return data ? rowToProfile(data) : null;
|
|
32
|
+
}
|
|
33
|
+
async getProfiles(_ctx, userIds) {
|
|
34
|
+
// Batch read serves display-name lookups across the UI; scoping to one
|
|
35
|
+
// user defeats the purpose. No assertCanAccess.
|
|
36
|
+
if (userIds.length === 0)
|
|
37
|
+
return [];
|
|
38
|
+
const { data, error } = await this.client()
|
|
39
|
+
.from('user_profiles')
|
|
40
|
+
.select('user_id, display_name, starred_definitions, recent_runs')
|
|
41
|
+
.in('user_id', [...userIds]);
|
|
42
|
+
if (error)
|
|
43
|
+
throw mapError(error);
|
|
44
|
+
return (data ?? []).map(rowToProfile);
|
|
45
|
+
}
|
|
46
|
+
async updateProfile(ctx, userId, patch) {
|
|
47
|
+
assertCanAccess(ctx, userId);
|
|
48
|
+
const row = {};
|
|
49
|
+
if (patch.displayName !== undefined)
|
|
50
|
+
row.display_name = patch.displayName;
|
|
51
|
+
if (Object.keys(row).length === 0)
|
|
52
|
+
return 'ok';
|
|
53
|
+
const { data, error } = await this.client()
|
|
54
|
+
.from('user_profiles')
|
|
55
|
+
.update(row)
|
|
56
|
+
.eq('user_id', userId)
|
|
57
|
+
.select('user_id');
|
|
58
|
+
if (error)
|
|
59
|
+
throw mapError(error);
|
|
60
|
+
return data && data.length > 0 ? 'ok' : 'not_found';
|
|
61
|
+
}
|
|
62
|
+
async starDefinition(ctx, userId, definitionId) {
|
|
63
|
+
assertCanAccess(ctx, userId);
|
|
64
|
+
// Read-modify-write is fine for the local scale here; if concurrent
|
|
65
|
+
// starring ever becomes a hotspot, a SECURITY DEFINER RPC using
|
|
66
|
+
// `array_append` + `array_distinct` closes the race.
|
|
67
|
+
const existing = await this.getProfile(ctx, userId);
|
|
68
|
+
if (!existing)
|
|
69
|
+
return 'not_found';
|
|
70
|
+
if (existing.starredDefinitions.includes(definitionId))
|
|
71
|
+
return 'ok';
|
|
72
|
+
const next = [...existing.starredDefinitions, definitionId];
|
|
73
|
+
const { data, error } = await this.client()
|
|
74
|
+
.from('user_profiles')
|
|
75
|
+
.update({ starred_definitions: next })
|
|
76
|
+
.eq('user_id', userId)
|
|
77
|
+
.select('user_id');
|
|
78
|
+
if (error)
|
|
79
|
+
throw mapError(error);
|
|
80
|
+
return data && data.length > 0 ? 'ok' : 'not_found';
|
|
81
|
+
}
|
|
82
|
+
async unstarDefinition(ctx, userId, definitionId) {
|
|
83
|
+
assertCanAccess(ctx, userId);
|
|
84
|
+
const existing = await this.getProfile(ctx, userId);
|
|
85
|
+
if (!existing)
|
|
86
|
+
return 'not_found';
|
|
87
|
+
if (!existing.starredDefinitions.includes(definitionId))
|
|
88
|
+
return 'ok';
|
|
89
|
+
const next = existing.starredDefinitions.filter((id) => id !== definitionId);
|
|
90
|
+
const { data, error } = await this.client()
|
|
91
|
+
.from('user_profiles')
|
|
92
|
+
.update({ starred_definitions: next })
|
|
93
|
+
.eq('user_id', userId)
|
|
94
|
+
.select('user_id');
|
|
95
|
+
if (error)
|
|
96
|
+
throw mapError(error);
|
|
97
|
+
return data && data.length > 0 ? 'ok' : 'not_found';
|
|
98
|
+
}
|
|
99
|
+
async recordRun(ctx, userId, run) {
|
|
100
|
+
assertCanAccess(ctx, userId);
|
|
101
|
+
const existing = await this.getProfile(ctx, userId);
|
|
102
|
+
if (!existing)
|
|
103
|
+
return 'not_found';
|
|
104
|
+
// Prepend newest, de-dupe by definitionId, cap at MAX_RECENT_RUNS.
|
|
105
|
+
const filtered = existing.recentRuns.filter((r) => r.definitionId !== run.definitionId);
|
|
106
|
+
const next = [run, ...filtered].slice(0, MAX_RECENT_RUNS);
|
|
107
|
+
const { data, error } = await this.client()
|
|
108
|
+
.from('user_profiles')
|
|
109
|
+
.update({ recent_runs: next })
|
|
110
|
+
.eq('user_id', userId)
|
|
111
|
+
.select('user_id');
|
|
112
|
+
if (error)
|
|
113
|
+
throw mapError(error);
|
|
114
|
+
return data && data.length > 0 ? 'ok' : 'not_found';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function assertCanAccess(ctx, userId) {
|
|
118
|
+
if (ctx.system)
|
|
119
|
+
return;
|
|
120
|
+
if (ctx.userId === userId)
|
|
121
|
+
return;
|
|
122
|
+
if (hasPermission(ctx, 'instance_admin'))
|
|
123
|
+
return;
|
|
124
|
+
throw new ProviderError('Forbidden: cannot access another user’s profile', 403);
|
|
125
|
+
}
|
|
126
|
+
function rowToProfile(row) {
|
|
127
|
+
return {
|
|
128
|
+
userId: row.user_id,
|
|
129
|
+
displayName: row.display_name ?? undefined,
|
|
130
|
+
starredDefinitions: row.starred_definitions ?? [],
|
|
131
|
+
recentRuns: row.recent_runs ?? []
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function mapError(e) {
|
|
135
|
+
const pg = e;
|
|
136
|
+
if (pg?.code === '23505')
|
|
137
|
+
return new ProviderError(pg.message ?? 'Duplicate record', 409);
|
|
138
|
+
if (pg?.code === '23503')
|
|
139
|
+
return new ProviderError(pg.message ?? 'Foreign key violation', 409);
|
|
140
|
+
if (e instanceof Error)
|
|
141
|
+
return e;
|
|
142
|
+
if (e && typeof e === 'object') {
|
|
143
|
+
const obj = e;
|
|
144
|
+
return new Error(obj.code ? `[${obj.code}] ${obj.message ?? ''}` : (obj.message ?? String(e)));
|
|
145
|
+
}
|
|
146
|
+
return new Error(String(e));
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=SupabaseUserProfileProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SupabaseUserProfileProvider.js","sourceRoot":"","sources":["../../src/userProfile/SupabaseUserProfileProvider.ts"],"names":[],"mappings":"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"}
|