@selvajs/supabase-provider 0.14.3 → 0.14.4-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/dist/auth/SupabaseAuthProvider.d.ts +57 -0
- package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
- package/dist/auth/SupabaseAuthProvider.js +123 -18
- package/dist/auth/SupabaseAuthProvider.js.map +1 -1
- package/dist/data/SupabaseComputeServerStore.d.ts +34 -2
- package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -1
- package/dist/data/SupabaseComputeServerStore.js +146 -39
- package/dist/data/SupabaseComputeServerStore.js.map +1 -1
- package/dist/data/SupabaseDataProvider.d.ts +18 -3
- package/dist/data/SupabaseDataProvider.d.ts.map +1 -1
- package/dist/data/SupabaseDataProvider.js +83 -18
- package/dist/data/SupabaseDataProvider.js.map +1 -1
- package/dist/data/SupabaseDefinitionStore.d.ts +3 -1
- package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -1
- package/dist/data/SupabaseDefinitionStore.js +94 -67
- package/dist/data/SupabaseDefinitionStore.js.map +1 -1
- package/dist/data/SupabaseInviteStore.d.ts.map +1 -1
- package/dist/data/SupabaseInviteStore.js +11 -22
- package/dist/data/SupabaseInviteStore.js.map +1 -1
- package/dist/data/SupabaseOrgStore.d.ts.map +1 -1
- package/dist/data/SupabaseOrgStore.js +49 -86
- package/dist/data/SupabaseOrgStore.js.map +1 -1
- package/dist/data/SupabaseProjectStore.d.ts.map +1 -1
- package/dist/data/SupabaseProjectStore.js +51 -70
- package/dist/data/SupabaseProjectStore.js.map +1 -1
- package/dist/data/SupabaseShareLinkStore.d.ts.map +1 -1
- package/dist/data/SupabaseShareLinkStore.js +13 -27
- package/dist/data/SupabaseShareLinkStore.js.map +1 -1
- package/dist/data/client.d.ts +35 -3
- package/dist/data/client.d.ts.map +1 -1
- package/dist/data/client.js +67 -16
- package/dist/data/client.js.map +1 -1
- package/dist/data/errors.d.ts +2 -0
- package/dist/data/errors.d.ts.map +1 -0
- package/dist/data/errors.js +23 -0
- package/dist/data/errors.js.map +1 -0
- package/dist/data/index.d.ts +8 -2
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +6 -1
- package/dist/data/index.js.map +1 -1
- package/dist/data/migrationHead.d.ts +14 -0
- package/dist/data/migrationHead.d.ts.map +1 -0
- package/dist/data/migrationHead.js +14 -0
- package/dist/data/migrationHead.js.map +1 -0
- package/dist/data/query.d.ts +20 -0
- package/dist/data/query.d.ts.map +1 -0
- package/dist/data/query.js +13 -0
- package/dist/data/query.js.map +1 -0
- package/dist/data/rowStamp.d.ts +38 -0
- package/dist/data/rowStamp.d.ts.map +1 -0
- package/dist/data/rowStamp.js +43 -0
- package/dist/data/rowStamp.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/permissions/SupabasePlatformPermissionStore.d.ts +5 -3
- package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -1
- package/dist/permissions/SupabasePlatformPermissionStore.js +17 -30
- package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -1
- package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
- package/dist/storage/SupabaseStorageProvider.js +3 -0
- package/dist/storage/SupabaseStorageProvider.js.map +1 -1
- package/package.json +13 -14
- package/supabase/.branches/_current_branch +1 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/migrations/20260711120000_selva_definition_schema_version.sql +18 -0
- package/supabase/migrations/20260712120000_user_profiles_disabled_and_gin.sql +95 -0
- package/supabase/migrations/20260712123000_selva_migration_head_fn.sql +38 -0
- package/supabase/migrations/20260713120000_selva_next_version_number.sql +59 -0
- package/supabase/migrations/20260713140000_selva_solve_cache_limit.sql +15 -0
- package/dist/auth/__tests__/conformance.test.d.ts +0 -2
- package/dist/auth/__tests__/conformance.test.d.ts.map +0 -1
- package/dist/auth/__tests__/conformance.test.js +0 -76
- package/dist/auth/__tests__/conformance.test.js.map +0 -1
- package/dist/data/__tests__/compute-server-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/compute-server-conformance.test.js +0 -40
- package/dist/data/__tests__/compute-server-conformance.test.js.map +0 -1
- package/dist/data/__tests__/definition-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/definition-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/definition-conformance.test.js +0 -85
- package/dist/data/__tests__/definition-conformance.test.js.map +0 -1
- package/dist/data/__tests__/event-sink-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/event-sink-conformance.test.js +0 -27
- package/dist/data/__tests__/event-sink-conformance.test.js.map +0 -1
- package/dist/data/__tests__/invite-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/invite-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/invite-conformance.test.js +0 -45
- package/dist/data/__tests__/invite-conformance.test.js.map +0 -1
- package/dist/data/__tests__/org-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/org-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/org-conformance.test.js +0 -29
- package/dist/data/__tests__/org-conformance.test.js.map +0 -1
- package/dist/data/__tests__/project-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/project-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/project-conformance.test.js +0 -57
- package/dist/data/__tests__/project-conformance.test.js.map +0 -1
- package/dist/data/__tests__/share-link-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/share-link-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/share-link-conformance.test.js +0 -88
- package/dist/data/__tests__/share-link-conformance.test.js.map +0 -1
- package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/solve-metric-sink-conformance.test.js +0 -52
- package/dist/data/__tests__/solve-metric-sink-conformance.test.js.map +0 -1
- package/dist/data/__tests__/test-helpers.d.ts +0 -52
- package/dist/data/__tests__/test-helpers.d.ts.map +0 -1
- package/dist/data/__tests__/test-helpers.js +0 -177
- package/dist/data/__tests__/test-helpers.js.map +0 -1
- package/dist/permissions/__tests__/conformance.test.d.ts +0 -2
- package/dist/permissions/__tests__/conformance.test.d.ts.map +0 -1
- package/dist/permissions/__tests__/conformance.test.js +0 -27
- package/dist/permissions/__tests__/conformance.test.js.map +0 -1
- package/dist/storage/__tests__/conformance.test.d.ts +0 -2
- package/dist/storage/__tests__/conformance.test.d.ts.map +0 -1
- package/dist/storage/__tests__/conformance.test.js +0 -149
- package/dist/storage/__tests__/conformance.test.js.map +0 -1
- package/dist/userProfile/__tests__/conformance.test.d.ts +0 -2
- package/dist/userProfile/__tests__/conformance.test.d.ts.map +0 -1
- package/dist/userProfile/__tests__/conformance.test.js +0 -38
- package/dist/userProfile/__tests__/conformance.test.js.map +0 -1
- package/src/auth/SupabaseAuthProvider.ts +0 -464
- package/src/auth/__tests__/conformance.test.ts +0 -78
- package/src/auth/index.ts +0 -2
- package/src/cli/sync-migrations.ts +0 -133
- package/src/data/SupabaseAuditQuery.ts +0 -103
- package/src/data/SupabaseComputeServerStore.ts +0 -262
- package/src/data/SupabaseDataProvider.ts +0 -133
- package/src/data/SupabaseDefinitionStore.ts +0 -520
- package/src/data/SupabaseEventSink.ts +0 -30
- package/src/data/SupabaseInviteStore.ts +0 -183
- package/src/data/SupabaseOrgStore.ts +0 -552
- package/src/data/SupabaseProjectStore.ts +0 -454
- package/src/data/SupabaseShareLinkStore.ts +0 -191
- package/src/data/SupabaseSolveMetricSink.ts +0 -38
- package/src/data/__tests__/compute-server-conformance.test.ts +0 -40
- package/src/data/__tests__/definition-conformance.test.ts +0 -89
- package/src/data/__tests__/event-sink-conformance.test.ts +0 -32
- package/src/data/__tests__/invite-conformance.test.ts +0 -46
- package/src/data/__tests__/org-conformance.test.ts +0 -30
- package/src/data/__tests__/project-conformance.test.ts +0 -57
- package/src/data/__tests__/share-link-conformance.test.ts +0 -87
- package/src/data/__tests__/solve-metric-sink-conformance.test.ts +0 -59
- package/src/data/__tests__/test-helpers.ts +0 -188
- package/src/data/client.ts +0 -98
- package/src/data/index.ts +0 -12
- package/src/data/pagination.ts +0 -68
- package/src/index.ts +0 -24
- package/src/permissions/SupabasePlatformPermissionStore.ts +0 -161
- package/src/permissions/__tests__/conformance.test.ts +0 -28
- package/src/permissions/index.ts +0 -1
- package/src/storage/SupabaseStorageProvider.ts +0 -182
- package/src/storage/__tests__/conformance.test.ts +0 -152
- package/src/storage/index.ts +0 -2
- package/src/userProfile/SupabaseUserProfileProvider.ts +0 -177
- package/src/userProfile/__tests__/conformance.test.ts +0 -39
- package/src/userProfile/index.ts +0 -1
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { describe, beforeEach, it } from 'vitest';
|
|
2
|
-
import { runAuthProviderConformance, runEmailLinkAuthConformance } from '@selvajs/platform/testing';
|
|
3
|
-
import { SupabaseAuthProvider } from '../SupabaseAuthProvider.js';
|
|
4
|
-
import { readEnv, resetAllData } from '../../data/__tests__/test-helpers.js';
|
|
5
|
-
|
|
6
|
-
const envCtx = readEnv();
|
|
7
|
-
|
|
8
|
-
if (!envCtx) {
|
|
9
|
-
describe.skip('SupabaseAuthProvider (skipped: no live stack)', () => {
|
|
10
|
-
it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => {});
|
|
11
|
-
});
|
|
12
|
-
} else {
|
|
13
|
-
const ctx = envCtx; // non-null in this branch
|
|
14
|
-
// The auth conformance suite expects `verifyLogin('', adminPassword)` to
|
|
15
|
-
// succeed. Supabase requires a non-empty email, so we seed a deterministic
|
|
16
|
-
// admin email and have the suite use that via the `adminEmail` option.
|
|
17
|
-
const ADMIN_EMAIL = 'admin@conformance.test';
|
|
18
|
-
const ADMIN_PASSWORD = 'conformance-admin-password-1234';
|
|
19
|
-
|
|
20
|
-
async function ensureAdmin() {
|
|
21
|
-
const { data: existing } = await ctx.adminClient.auth.admin.listUsers({ perPage: 1000 });
|
|
22
|
-
const match = existing.users.find((u) => u.email === ADMIN_EMAIL);
|
|
23
|
-
if (match) return match.id;
|
|
24
|
-
const { data, error } = await ctx.adminClient.auth.admin.createUser({
|
|
25
|
-
email: ADMIN_EMAIL,
|
|
26
|
-
password: ADMIN_PASSWORD,
|
|
27
|
-
email_confirm: true
|
|
28
|
-
});
|
|
29
|
-
if (error) throw error;
|
|
30
|
-
return data.user.id;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function makeProvider() {
|
|
34
|
-
const provider = new SupabaseAuthProvider({
|
|
35
|
-
supabaseUrl: ctx.url,
|
|
36
|
-
anonKey: ctx.anonKey,
|
|
37
|
-
serviceRoleKey: ctx.serviceRoleKey
|
|
38
|
-
});
|
|
39
|
-
// Ensure the admin account exists + has instance_admin.
|
|
40
|
-
const id = await ensureAdmin();
|
|
41
|
-
const { error: upError } = await ctx.adminClient
|
|
42
|
-
.from('user_profiles')
|
|
43
|
-
.update({ platform_permissions: ['instance_admin'] })
|
|
44
|
-
.eq('user_id', id);
|
|
45
|
-
if (upError) throw upError;
|
|
46
|
-
return { provider, adminPassword: ADMIN_PASSWORD };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
describe('SupabaseAuthProvider', () => {
|
|
50
|
-
beforeEach(async () => {
|
|
51
|
-
await resetAllData(ctx);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
runAuthProviderConformance({
|
|
55
|
-
name: 'SupabaseAuthProvider',
|
|
56
|
-
createProvider: async () => {
|
|
57
|
-
const { provider, adminPassword } = await makeProvider();
|
|
58
|
-
return { provider, adminPassword, adminEmail: ADMIN_EMAIL };
|
|
59
|
-
},
|
|
60
|
-
userManagement: true
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
runEmailLinkAuthConformance({
|
|
64
|
-
name: 'SupabaseEmailLinkAuth',
|
|
65
|
-
createAdapter: async () => {
|
|
66
|
-
const { provider } = await makeProvider();
|
|
67
|
-
return {
|
|
68
|
-
adapter: provider.emailLink,
|
|
69
|
-
// `validEmail` isn't used by the suite for live mail delivery;
|
|
70
|
-
// it just needs to look like a real address. The malformed
|
|
71
|
-
// case is what we actually assert on.
|
|
72
|
-
validEmail: 'real-looking@conformance.test',
|
|
73
|
-
invalidEmail: 'not-an-email-address'
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
}
|
package/src/auth/index.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// ============================================================================
|
|
3
|
-
// sync-migrations — copy this package's Supabase migrations into a consuming
|
|
4
|
-
// app's supabase/migrations/ directory.
|
|
5
|
-
//
|
|
6
|
-
// Migrations ship with deterministic timestamp-prefixed filenames
|
|
7
|
-
// (<UTCtimestamp>_selva_<name>.sql) so they slot into a consuming app's own
|
|
8
|
-
// migration timeline without colliding, and so every machine/CI produces a
|
|
9
|
-
// byte-identical migration history. We copy verbatim — no rename at copy time,
|
|
10
|
-
// which is what keeps the history stable.
|
|
11
|
-
//
|
|
12
|
-
// Usage:
|
|
13
|
-
// npx @selvajs/supabase-provider sync-migrations [--dir <path>] [--force]
|
|
14
|
-
//
|
|
15
|
-
// --dir <path> Target migrations dir (default: ./supabase/migrations)
|
|
16
|
-
// --force Overwrite files that exist with differing content
|
|
17
|
-
// ============================================================================
|
|
18
|
-
|
|
19
|
-
import { fileURLToPath } from 'node:url';
|
|
20
|
-
import { dirname, join, resolve } from 'node:path';
|
|
21
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
22
|
-
|
|
23
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
|
|
25
|
-
// dist/cli/ -> package root is two levels up; migrations ship alongside src/dist.
|
|
26
|
-
const packageRoot = resolve(here, '..', '..');
|
|
27
|
-
const sourceDir = join(packageRoot, 'supabase', 'migrations');
|
|
28
|
-
|
|
29
|
-
interface Options {
|
|
30
|
-
targetDir: string;
|
|
31
|
-
force: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function parseArgs(argv: string[]): Options {
|
|
35
|
-
let targetDir = resolve(process.cwd(), 'supabase', 'migrations');
|
|
36
|
-
let force = false;
|
|
37
|
-
|
|
38
|
-
for (let i = 0; i < argv.length; i++) {
|
|
39
|
-
const arg = argv[i];
|
|
40
|
-
if (arg === '--dir') {
|
|
41
|
-
const value = argv[++i];
|
|
42
|
-
if (!value) fail('--dir requires a path argument');
|
|
43
|
-
targetDir = resolve(process.cwd(), value);
|
|
44
|
-
} else if (arg === '--force') {
|
|
45
|
-
force = true;
|
|
46
|
-
} else if (arg === '--help' || arg === '-h') {
|
|
47
|
-
printHelp();
|
|
48
|
-
process.exit(0);
|
|
49
|
-
} else {
|
|
50
|
-
fail(`unknown argument: ${arg}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return { targetDir, force };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function printHelp(): void {
|
|
58
|
-
console.log(
|
|
59
|
-
[
|
|
60
|
-
'sync-migrations — copy @selvajs/supabase-provider migrations into your app.',
|
|
61
|
-
'',
|
|
62
|
-
'Usage:',
|
|
63
|
-
' npx @selvajs/supabase-provider sync-migrations [--dir <path>] [--force]',
|
|
64
|
-
'',
|
|
65
|
-
' --dir <path> Target migrations dir (default: ./supabase/migrations)',
|
|
66
|
-
' --force Overwrite files that exist with differing content',
|
|
67
|
-
'',
|
|
68
|
-
'After syncing, apply with: npx supabase db push'
|
|
69
|
-
].join('\n')
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function fail(message: string): never {
|
|
74
|
-
console.error(`error: ${message}`);
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function main(): void {
|
|
79
|
-
const { targetDir, force } = parseArgs(process.argv.slice(2));
|
|
80
|
-
|
|
81
|
-
if (!existsSync(sourceDir)) {
|
|
82
|
-
fail(`could not locate packaged migrations at ${sourceDir}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const files = readdirSync(sourceDir)
|
|
86
|
-
.filter((name) => name.endsWith('.sql'))
|
|
87
|
-
.sort();
|
|
88
|
-
|
|
89
|
-
if (files.length === 0) {
|
|
90
|
-
fail(`no .sql migrations found in ${sourceDir}`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
mkdirSync(targetDir, { recursive: true });
|
|
94
|
-
|
|
95
|
-
let copied = 0;
|
|
96
|
-
let skipped = 0;
|
|
97
|
-
let conflicts = 0;
|
|
98
|
-
|
|
99
|
-
for (const name of files) {
|
|
100
|
-
const from = join(sourceDir, name);
|
|
101
|
-
const to = join(targetDir, name);
|
|
102
|
-
const contents = readFileSync(from);
|
|
103
|
-
|
|
104
|
-
if (existsSync(to)) {
|
|
105
|
-
const existing = readFileSync(to);
|
|
106
|
-
if (existing.equals(contents)) {
|
|
107
|
-
skipped++;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (!force) {
|
|
111
|
-
console.warn(
|
|
112
|
-
` conflict ${name} exists with different content (use --force to overwrite)`
|
|
113
|
-
);
|
|
114
|
-
conflicts++;
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
writeFileSync(to, contents);
|
|
120
|
-
console.log(` copied ${name}`);
|
|
121
|
-
copied++;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
console.log(`\n${copied} copied, ${skipped} unchanged, ${conflicts} conflict(s) -> ${targetDir}`);
|
|
125
|
-
|
|
126
|
-
if (conflicts > 0) {
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
console.log('\nNext: review the files, then run npx supabase db push');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
main();
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AuditEventRow,
|
|
3
|
-
AuditQueryFilters,
|
|
4
|
-
AuditQueryResult,
|
|
5
|
-
DomainEvent,
|
|
6
|
-
IAuditQuery,
|
|
7
|
-
RequestContext
|
|
8
|
-
} from '@selvajs/platform';
|
|
9
|
-
import type { ClientBundle } from './client.js';
|
|
10
|
-
|
|
11
|
-
const MAX_LIMIT = 200;
|
|
12
|
-
const DEFAULT_LIMIT = 100;
|
|
13
|
-
|
|
14
|
-
// Cursor values are interpolated into PostgREST filter strings — both formats
|
|
15
|
-
// must be validated before any string concatenation reaches the query.
|
|
16
|
-
const ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
17
|
-
const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Read-side for `audit_events`. Always uses the service-role client — RLS
|
|
21
|
-
* blocks all read access by design, so the route layer is the single trust
|
|
22
|
-
* boundary (callers must gate on `instance_admin`).
|
|
23
|
-
*
|
|
24
|
-
* Pagination is keyset on `(occurred_at desc, id desc)`. The `id` tie-breaker
|
|
25
|
-
* prevents skipped rows when multiple events land in the same millisecond.
|
|
26
|
-
*/
|
|
27
|
-
export class SupabaseAuditQuery implements IAuditQuery {
|
|
28
|
-
constructor(private readonly clients: ClientBundle) {}
|
|
29
|
-
|
|
30
|
-
async list(_ctx: RequestContext, filters: AuditQueryFilters): Promise<AuditQueryResult> {
|
|
31
|
-
const limit = clampLimit(filters.limit);
|
|
32
|
-
|
|
33
|
-
let q = this.clients.serviceClient
|
|
34
|
-
.from('audit_events')
|
|
35
|
-
.select('id, type, actor_id, occurred_at, data')
|
|
36
|
-
.order('occurred_at', { ascending: false })
|
|
37
|
-
.order('id', { ascending: false })
|
|
38
|
-
// Fetch one extra row to detect "has next page" without a count query.
|
|
39
|
-
.limit(limit + 1);
|
|
40
|
-
|
|
41
|
-
if (filters.types && filters.types.length > 0) {
|
|
42
|
-
q = q.in('type', [...filters.types]);
|
|
43
|
-
}
|
|
44
|
-
if (filters.actorId) {
|
|
45
|
-
q = q.eq('actor_id', filters.actorId);
|
|
46
|
-
}
|
|
47
|
-
if (filters.sinceIso) {
|
|
48
|
-
q = q.gte('occurred_at', filters.sinceIso);
|
|
49
|
-
}
|
|
50
|
-
if (filters.untilIso) {
|
|
51
|
-
q = q.lte('occurred_at', filters.untilIso);
|
|
52
|
-
}
|
|
53
|
-
if (filters.cursor) {
|
|
54
|
-
// `(occurred_at, id) < (cursorTs, cursorId)` expressed for PostgREST:
|
|
55
|
-
// strictly older OR same timestamp with a strictly smaller id.
|
|
56
|
-
//
|
|
57
|
-
// Both values are interpolated into a `.or()` filter string. Validate
|
|
58
|
-
// shape before interpolation — without these guards a malformed cursor
|
|
59
|
-
// (or a hand-crafted URL by a route caller) could break out of the
|
|
60
|
-
// filter and combine with arbitrary PostgREST operators.
|
|
61
|
-
const ts = filters.cursor.occurredAt;
|
|
62
|
-
const id = filters.cursor.id;
|
|
63
|
-
if (!ISO_RE.test(ts) || !UUID_RE.test(id)) {
|
|
64
|
-
throw new Error('audit_events query: malformed cursor');
|
|
65
|
-
}
|
|
66
|
-
q = q.or(`occurred_at.lt.${ts},and(occurred_at.eq.${ts},id.lt.${id})`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const { data, error } = await q;
|
|
70
|
-
if (error) {
|
|
71
|
-
throw new Error(`audit_events query failed: ${error.message}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const fetched = (data ?? []) as Array<{
|
|
75
|
-
id: string;
|
|
76
|
-
type: string;
|
|
77
|
-
actor_id: string;
|
|
78
|
-
occurred_at: string;
|
|
79
|
-
data: DomainEvent;
|
|
80
|
-
}>;
|
|
81
|
-
|
|
82
|
-
const hasMore = fetched.length > limit;
|
|
83
|
-
const page = hasMore ? fetched.slice(0, limit) : fetched;
|
|
84
|
-
const last = page[page.length - 1];
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
rows: page.map((r) => ({
|
|
88
|
-
id: r.id,
|
|
89
|
-
type: r.type as DomainEvent['type'],
|
|
90
|
-
actorId: r.actor_id,
|
|
91
|
-
occurredAt: r.occurred_at,
|
|
92
|
-
data: r.data
|
|
93
|
-
})) satisfies AuditEventRow[],
|
|
94
|
-
nextCursor: hasMore && last ? { occurredAt: last.occurred_at, id: last.id } : null
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function clampLimit(requested: number | undefined): number {
|
|
100
|
-
if (!requested || requested <= 0) return DEFAULT_LIMIT;
|
|
101
|
-
if (requested > MAX_LIMIT) return MAX_LIMIT;
|
|
102
|
-
return Math.floor(requested);
|
|
103
|
-
}
|
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isOrgServer,
|
|
3
|
-
isPlatformServer,
|
|
4
|
-
type IComputeServerStore,
|
|
5
|
-
type ComputeConfig,
|
|
6
|
-
type ComputeServerConfig,
|
|
7
|
-
type RequestContext
|
|
8
|
-
} from '@selvajs/platform';
|
|
9
|
-
import { ProviderError } from '@selvajs/platform';
|
|
10
|
-
import type { ClientBundle } from './client.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Compute-server config backed by three tables:
|
|
14
|
-
* - `compute_servers`: one row per server. `scope = 'platform' | 'org'`.
|
|
15
|
-
* Platform rows carry a `shared_with_all` flag + `compute_server_shares`
|
|
16
|
-
* allowlist; org rows carry `owner_org_id`.
|
|
17
|
-
* - `compute_server_shares`: many-to-many for platform servers shared
|
|
18
|
-
* with specific orgs (when `shared_with_all = false`).
|
|
19
|
-
* - `compute_server_org_defaults`: per-org `orgDefaults[orgId]` choice.
|
|
20
|
-
* - `compute_server_platform_default`: single-row sentinel for the global
|
|
21
|
-
* `defaultServerId`.
|
|
22
|
-
*
|
|
23
|
-
* `getConfig` returns the full doc; the visibility predicate is applied by
|
|
24
|
-
* callers (resolver, page loaders) using helpers in `@selvajs/platform`.
|
|
25
|
-
*/
|
|
26
|
-
export class SupabaseComputeServerStore implements IComputeServerStore {
|
|
27
|
-
constructor(private readonly clients: ClientBundle) {}
|
|
28
|
-
|
|
29
|
-
async getConfig(ctx: RequestContext): Promise<ComputeConfig> {
|
|
30
|
-
const client = this.clients.forRequest(ctx);
|
|
31
|
-
|
|
32
|
-
const [serversRes, sharesRes, orgDefRes, platDefRes] = await Promise.all([
|
|
33
|
-
client.from('compute_servers').select('*'),
|
|
34
|
-
client.from('compute_server_shares').select('*'),
|
|
35
|
-
client.from('compute_server_org_defaults').select('*'),
|
|
36
|
-
client
|
|
37
|
-
.from('compute_server_platform_default')
|
|
38
|
-
.select('default_server_id')
|
|
39
|
-
.eq('singleton', true)
|
|
40
|
-
.maybeSingle()
|
|
41
|
-
]);
|
|
42
|
-
|
|
43
|
-
if (serversRes.error) throw mapError(serversRes.error);
|
|
44
|
-
if (sharesRes.error) throw mapError(sharesRes.error);
|
|
45
|
-
if (orgDefRes.error) throw mapError(orgDefRes.error);
|
|
46
|
-
if (platDefRes.error) throw mapError(platDefRes.error);
|
|
47
|
-
|
|
48
|
-
// Build per-server `sharedWith` allowlists from the join table.
|
|
49
|
-
const sharedByServer = new Map<string, string[]>();
|
|
50
|
-
for (const row of sharesRes.data ?? []) {
|
|
51
|
-
const list = sharedByServer.get(row.server_id) ?? [];
|
|
52
|
-
list.push(row.org_id);
|
|
53
|
-
sharedByServer.set(row.server_id, list);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const servers = (serversRes.data ?? []).map((row) => rowToServer(row, sharedByServer));
|
|
57
|
-
|
|
58
|
-
const orgDefaults: Record<string, string> = {};
|
|
59
|
-
for (const row of orgDefRes.data ?? []) {
|
|
60
|
-
if (row.default_server_id) orgDefaults[row.org_id] = row.default_server_id;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
servers,
|
|
65
|
-
defaultServerId: platDefRes.data?.default_server_id ?? undefined,
|
|
66
|
-
orgDefaults
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async savePlatformServers(
|
|
71
|
-
ctx: RequestContext,
|
|
72
|
-
servers: ComputeServerConfig[],
|
|
73
|
-
defaultServerId: string | undefined
|
|
74
|
-
): Promise<void> {
|
|
75
|
-
const client = this.clients.forRequest(ctx);
|
|
76
|
-
const platformOnly = servers.filter(isPlatformServer);
|
|
77
|
-
|
|
78
|
-
// Replace-all of platform rows.
|
|
79
|
-
const { error: delErr } = await client.from('compute_servers').delete().eq('scope', 'platform');
|
|
80
|
-
if (delErr) throw mapError(delErr);
|
|
81
|
-
|
|
82
|
-
if (platformOnly.length > 0) {
|
|
83
|
-
const { error: insErr } = await client
|
|
84
|
-
.from('compute_servers')
|
|
85
|
-
.insert(platformOnly.map(serverToRow));
|
|
86
|
-
if (insErr) throw mapError(insErr);
|
|
87
|
-
|
|
88
|
-
// Rebuild the share rows for any non-`all` platform servers.
|
|
89
|
-
const shareRows = platformOnly.flatMap((s) =>
|
|
90
|
-
s.sharedWith === 'all'
|
|
91
|
-
? []
|
|
92
|
-
: s.sharedWith.map((orgId) => ({ server_id: s.id, org_id: orgId }))
|
|
93
|
-
);
|
|
94
|
-
if (shareRows.length > 0) {
|
|
95
|
-
const { error: shErr } = await client.from('compute_server_shares').insert(shareRows);
|
|
96
|
-
if (shErr) throw mapError(shErr);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const { error: defErr } = await client
|
|
101
|
-
.from('compute_server_platform_default')
|
|
102
|
-
.update({ default_server_id: defaultServerId ?? null })
|
|
103
|
-
.eq('singleton', true);
|
|
104
|
-
if (defErr) throw mapError(defErr);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async saveOrgServers(
|
|
108
|
-
ctx: RequestContext,
|
|
109
|
-
orgId: string,
|
|
110
|
-
servers: ComputeServerConfig[],
|
|
111
|
-
defaultServerId?: string | null
|
|
112
|
-
): Promise<void> {
|
|
113
|
-
const client = this.clients.forRequest(ctx);
|
|
114
|
-
const orgOnly = servers.filter(isOrgServer).map((s) => ({ ...s, ownerOrgId: orgId }));
|
|
115
|
-
|
|
116
|
-
const { error: delErr } = await client
|
|
117
|
-
.from('compute_servers')
|
|
118
|
-
.delete()
|
|
119
|
-
.eq('scope', 'org')
|
|
120
|
-
.eq('owner_org_id', orgId);
|
|
121
|
-
if (delErr) throw mapError(delErr);
|
|
122
|
-
|
|
123
|
-
if (orgOnly.length > 0) {
|
|
124
|
-
const { error: insErr } = await client
|
|
125
|
-
.from('compute_servers')
|
|
126
|
-
.insert(orgOnly.map(serverToRow));
|
|
127
|
-
if (insErr) throw mapError(insErr);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (defaultServerId === null) {
|
|
131
|
-
const { error } = await client
|
|
132
|
-
.from('compute_server_org_defaults')
|
|
133
|
-
.delete()
|
|
134
|
-
.eq('org_id', orgId);
|
|
135
|
-
if (error) throw mapError(error);
|
|
136
|
-
} else if (typeof defaultServerId === 'string') {
|
|
137
|
-
const { error } = await client
|
|
138
|
-
.from('compute_server_org_defaults')
|
|
139
|
-
.upsert({ org_id: orgId, default_server_id: defaultServerId });
|
|
140
|
-
if (error) throw mapError(error);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async setOrgDefault(ctx: RequestContext, orgId: string, serverId: string | null): Promise<void> {
|
|
145
|
-
const client = this.clients.forRequest(ctx);
|
|
146
|
-
if (serverId === null) {
|
|
147
|
-
const { error } = await client
|
|
148
|
-
.from('compute_server_org_defaults')
|
|
149
|
-
.delete()
|
|
150
|
-
.eq('org_id', orgId);
|
|
151
|
-
if (error) throw mapError(error);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const { error } = await client
|
|
155
|
-
.from('compute_server_org_defaults')
|
|
156
|
-
.upsert({ org_id: orgId, default_server_id: serverId });
|
|
157
|
-
if (error) throw mapError(error);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async deleteByOrg(ctx: RequestContext, orgId: string): Promise<void> {
|
|
161
|
-
const client = this.clients.forRequest(ctx);
|
|
162
|
-
|
|
163
|
-
// orgDefaults entry — FK on org_id cascades from orgs(id), but org
|
|
164
|
-
// soft-delete doesn't trigger the cascade, so do it explicitly.
|
|
165
|
-
const { error: defErr } = await client
|
|
166
|
-
.from('compute_server_org_defaults')
|
|
167
|
-
.delete()
|
|
168
|
-
.eq('org_id', orgId);
|
|
169
|
-
if (defErr) throw mapError(defErr);
|
|
170
|
-
|
|
171
|
-
// Org-private servers owned by this org.
|
|
172
|
-
const { error: srvErr } = await client
|
|
173
|
-
.from('compute_servers')
|
|
174
|
-
.delete()
|
|
175
|
-
.eq('scope', 'org')
|
|
176
|
-
.eq('owner_org_id', orgId);
|
|
177
|
-
if (srvErr) throw mapError(srvErr);
|
|
178
|
-
|
|
179
|
-
// Strip this org from any platform server share allowlist.
|
|
180
|
-
const { error: shErr } = await client
|
|
181
|
-
.from('compute_server_shares')
|
|
182
|
-
.delete()
|
|
183
|
-
.eq('org_id', orgId);
|
|
184
|
-
if (shErr) throw mapError(shErr);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
interface ServerRow {
|
|
189
|
-
id: string;
|
|
190
|
-
scope: 'platform' | 'org';
|
|
191
|
-
owner_org_id: string | null;
|
|
192
|
-
shared_with_all: boolean;
|
|
193
|
-
label: string;
|
|
194
|
-
server_url: string;
|
|
195
|
-
api_key: string | null;
|
|
196
|
-
timeout_ms: number | null;
|
|
197
|
-
retry_count: number | null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function rowToServer(row: ServerRow, sharedByServer: Map<string, string[]>): ComputeServerConfig {
|
|
201
|
-
const common = {
|
|
202
|
-
id: row.id,
|
|
203
|
-
label: row.label,
|
|
204
|
-
serverUrl: row.server_url,
|
|
205
|
-
apiKey: row.api_key ?? undefined,
|
|
206
|
-
timeoutMs: row.timeout_ms ?? undefined,
|
|
207
|
-
retryCount: row.retry_count ?? undefined
|
|
208
|
-
};
|
|
209
|
-
if (row.scope === 'platform') {
|
|
210
|
-
return {
|
|
211
|
-
...common,
|
|
212
|
-
scope: 'platform',
|
|
213
|
-
sharedWith: row.shared_with_all ? 'all' : (sharedByServer.get(row.id) ?? [])
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
if (!row.owner_org_id) {
|
|
217
|
-
throw new Error(`compute_servers row ${row.id} has scope='org' but null owner_org_id`);
|
|
218
|
-
}
|
|
219
|
-
return { ...common, scope: 'org', ownerOrgId: row.owner_org_id };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function serverToRow(s: ComputeServerConfig): ServerRow {
|
|
223
|
-
const base = {
|
|
224
|
-
id: s.id,
|
|
225
|
-
label: s.label,
|
|
226
|
-
server_url: s.serverUrl,
|
|
227
|
-
api_key: s.apiKey ?? null,
|
|
228
|
-
timeout_ms: s.timeoutMs ?? null,
|
|
229
|
-
retry_count: s.retryCount ?? null
|
|
230
|
-
};
|
|
231
|
-
if (isPlatformServer(s)) {
|
|
232
|
-
return {
|
|
233
|
-
...base,
|
|
234
|
-
scope: 'platform',
|
|
235
|
-
owner_org_id: null,
|
|
236
|
-
shared_with_all: s.sharedWith === 'all'
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
return {
|
|
240
|
-
...base,
|
|
241
|
-
scope: 'org',
|
|
242
|
-
owner_org_id: s.ownerOrgId,
|
|
243
|
-
shared_with_all: false
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
interface PostgrestError {
|
|
248
|
-
code?: string;
|
|
249
|
-
message?: string;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function mapError(e: unknown): Error {
|
|
253
|
-
const pg = e as PostgrestError;
|
|
254
|
-
if (pg?.code === '23505') return new ProviderError(pg.message ?? 'Duplicate record', 409);
|
|
255
|
-
if (pg?.code === '23503') return new ProviderError(pg.message ?? 'Foreign key violation', 409);
|
|
256
|
-
if (e instanceof Error) return e;
|
|
257
|
-
if (e && typeof e === 'object') {
|
|
258
|
-
const obj = e as { message?: string; code?: string };
|
|
259
|
-
return new Error(obj.code ? `[${obj.code}] ${obj.message ?? ''}` : (obj.message ?? String(e)));
|
|
260
|
-
}
|
|
261
|
-
return new Error(String(e));
|
|
262
|
-
}
|