@selvajs/platform 0.14.2 → 0.15.0-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.d.ts +60 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +12 -0
- package/dist/auth.js.map +1 -0
- package/dist/compute.d.ts +47 -0
- package/dist/compute.d.ts.map +1 -0
- package/dist/compute.js +11 -0
- package/dist/compute.js.map +1 -0
- package/dist/computeServer/index.d.ts +2 -0
- package/dist/computeServer/index.d.ts.map +1 -1
- package/dist/computeServer/index.js +6 -0
- package/dist/computeServer/index.js.map +1 -1
- package/dist/computeServer/interface.d.ts +10 -0
- package/dist/computeServer/interface.d.ts.map +1 -1
- package/dist/computeServer/secrets.d.ts +53 -0
- package/dist/computeServer/secrets.d.ts.map +1 -0
- package/dist/computeServer/secrets.js +67 -0
- package/dist/computeServer/secrets.js.map +1 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +13 -0
- package/dist/context.js.map +1 -1
- package/dist/data/interface.d.ts +33 -0
- package/dist/data/interface.d.ts.map +1 -1
- package/dist/definitions/interface.d.ts +18 -0
- package/dist/definitions/interface.d.ts.map +1 -1
- package/dist/definitions/schemas.d.ts +1 -0
- package/dist/definitions/schemas.d.ts.map +1 -1
- package/dist/definitions/schemas.js +3 -0
- package/dist/definitions/schemas.js.map +1 -1
- package/dist/definitions/service.d.ts +62 -0
- package/dist/definitions/service.d.ts.map +1 -0
- package/dist/definitions/service.js +149 -0
- package/dist/definitions/service.js.map +1 -0
- package/dist/definitions/types.d.ts +24 -1
- package/dist/definitions/types.d.ts.map +1 -1
- package/dist/definitions.d.ts +146 -0
- package/dist/definitions.d.ts.map +1 -0
- package/dist/definitions.js +23 -0
- package/dist/definitions.js.map +1 -0
- package/dist/errors/interface.d.ts +44 -0
- package/dist/errors/interface.d.ts.map +1 -0
- package/dist/errors/interface.js +9 -0
- package/dist/errors/interface.js.map +1 -0
- package/dist/events/interface.d.ts +23 -0
- package/dist/events/interface.d.ts.map +1 -1
- package/dist/events/interface.js.map +1 -1
- package/dist/index.d.ts +14 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/invites/schemas.d.ts +24 -0
- package/dist/invites/schemas.d.ts.map +1 -0
- package/dist/invites/schemas.js +17 -0
- package/dist/invites/schemas.js.map +1 -0
- package/dist/metrics/interface.d.ts +6 -0
- package/dist/metrics/interface.d.ts.map +1 -1
- package/dist/metrics/interface.js.map +1 -1
- package/dist/organizations/index.d.ts +4 -3
- package/dist/organizations/index.d.ts.map +1 -1
- package/dist/organizations/index.js +2 -1
- package/dist/organizations/index.js.map +1 -1
- package/dist/organizations/interface.d.ts +1 -1
- package/dist/organizations/interface.d.ts.map +1 -1
- package/dist/organizations/paths.d.ts +22 -0
- package/dist/organizations/paths.d.ts.map +1 -0
- package/dist/organizations/paths.js +43 -0
- package/dist/organizations/paths.js.map +1 -0
- package/dist/organizations/schemas.d.ts +10 -0
- package/dist/organizations/schemas.d.ts.map +1 -1
- package/dist/organizations/schemas.js +6 -0
- package/dist/organizations/schemas.js.map +1 -1
- package/dist/organizations/types.d.ts +10 -1
- package/dist/organizations/types.d.ts.map +1 -1
- package/dist/organizations.d.ts +93 -0
- package/dist/organizations.d.ts.map +1 -0
- package/dist/organizations.js +14 -0
- package/dist/organizations.js.map +1 -0
- package/dist/solveCache/interface.d.ts +86 -0
- package/dist/solveCache/interface.d.ts.map +1 -0
- package/dist/solveCache/interface.js +12 -0
- package/dist/solveCache/interface.js.map +1 -0
- package/dist/storage/assetClasses.d.ts +59 -0
- package/dist/storage/assetClasses.d.ts.map +1 -0
- package/dist/storage/assetClasses.js +86 -0
- package/dist/storage/assetClasses.js.map +1 -0
- package/dist/storage/cacheBust.d.ts +21 -0
- package/dist/storage/cacheBust.d.ts.map +1 -0
- package/dist/storage/cacheBust.js +29 -0
- package/dist/storage/cacheBust.js.map +1 -0
- package/dist/storage/image.d.ts +6 -1
- package/dist/storage/image.d.ts.map +1 -1
- package/dist/storage/image.js +8 -3
- package/dist/storage/image.js.map +1 -1
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/testing/adapterSuite.d.ts +33 -0
- package/dist/testing/adapterSuite.d.ts.map +1 -0
- package/dist/testing/adapterSuite.js +166 -0
- package/dist/testing/adapterSuite.js.map +1 -0
- package/dist/testing/suites/definitionStoreSuite.d.ts.map +1 -1
- package/dist/testing/suites/definitionStoreSuite.js +87 -0
- package/dist/testing/suites/definitionStoreSuite.js.map +1 -1
- package/dist/testing/suites/eventSinkSuite.js +1 -0
- package/dist/testing/suites/eventSinkSuite.js.map +1 -1
- package/dist/testing/suites/orgStoreSuite.d.ts.map +1 -1
- package/dist/testing/suites/orgStoreSuite.js +12 -0
- package/dist/testing/suites/orgStoreSuite.js.map +1 -1
- package/dist/testing/suites/platformPermissionStoreSuite.d.ts +9 -0
- package/dist/testing/suites/platformPermissionStoreSuite.d.ts.map +1 -1
- package/dist/testing/suites/platformPermissionStoreSuite.js +34 -1
- package/dist/testing/suites/platformPermissionStoreSuite.js.map +1 -1
- package/dist/testing/suites/runner.d.ts +21 -0
- package/dist/testing/suites/runner.d.ts.map +1 -0
- package/dist/testing/suites/runner.js +7 -0
- package/dist/testing/suites/runner.js.map +1 -0
- package/dist/testing/suites/storageProviderSuite.d.ts.map +1 -1
- package/dist/testing/suites/storageProviderSuite.js +18 -0
- package/dist/testing/suites/storageProviderSuite.js.map +1 -1
- package/package.json +9 -24
- package/src/access/index.ts +0 -23
- package/src/access/rules.ts +0 -276
- package/src/auth/index.ts +0 -8
- package/src/auth/interface.ts +0 -279
- package/src/auth/types.ts +0 -32
- package/src/bindings/index.ts +0 -2
- package/src/bindings/interface.ts +0 -61
- package/src/computeServer/index.ts +0 -17
- package/src/computeServer/interface.ts +0 -62
- package/src/computeServer/types.ts +0 -74
- package/src/computeServer/utils.ts +0 -112
- package/src/config.ts +0 -99
- package/src/context.ts +0 -66
- package/src/data/index.ts +0 -1
- package/src/data/interface.ts +0 -70
- package/src/definitions/index.ts +0 -26
- package/src/definitions/interface.ts +0 -78
- package/src/definitions/paths.ts +0 -31
- package/src/definitions/schemas.ts +0 -64
- package/src/definitions/types.ts +0 -111
- package/src/errors.ts +0 -16
- package/src/events/audit.ts +0 -67
- package/src/events/index.ts +0 -9
- package/src/events/interface.ts +0 -102
- package/src/index.ts +0 -219
- package/src/invites/index.ts +0 -2
- package/src/invites/interface.ts +0 -38
- package/src/invites/types.ts +0 -39
- package/src/metrics/interface.ts +0 -86
- package/src/organizations/index.ts +0 -14
- package/src/organizations/interface.ts +0 -68
- package/src/organizations/schemas.ts +0 -52
- package/src/organizations/types.ts +0 -31
- package/src/pagination.ts +0 -45
- package/src/permissions/index.ts +0 -3
- package/src/permissions/interface.ts +0 -60
- package/src/permissions/types.ts +0 -21
- package/src/platformProjects/index.ts +0 -3
- package/src/platformProjects/interface.ts +0 -27
- package/src/platformProjects/schemas.ts +0 -13
- package/src/platformProjects/types.ts +0 -22
- package/src/projects/index.ts +0 -4
- package/src/projects/interface.ts +0 -58
- package/src/projects/schemas.ts +0 -28
- package/src/projects/types.ts +0 -36
- package/src/shareLinks/index.ts +0 -5
- package/src/shareLinks/interface.ts +0 -35
- package/src/shareLinks/schemas.ts +0 -21
- package/src/shareLinks/types.ts +0 -34
- package/src/storage/image.ts +0 -77
- package/src/storage/index.ts +0 -9
- package/src/storage/interface.ts +0 -27
- package/src/testing/index.ts +0 -47
- package/src/testing/suites/authProviderSuite.ts +0 -207
- package/src/testing/suites/computeServerStoreSuite.ts +0 -195
- package/src/testing/suites/definitionStoreSuite.ts +0 -585
- package/src/testing/suites/emailLinkAuthSuite.ts +0 -83
- package/src/testing/suites/eventSinkSuite.ts +0 -379
- package/src/testing/suites/helpers.ts +0 -87
- package/src/testing/suites/inviteStoreSuite.ts +0 -143
- package/src/testing/suites/orgStoreSuite.ts +0 -412
- package/src/testing/suites/platformPermissionStoreSuite.ts +0 -196
- package/src/testing/suites/platformProjectGrantStoreSuite.ts +0 -166
- package/src/testing/suites/projectStoreSuite.ts +0 -381
- package/src/testing/suites/shareLinkStoreSuite.ts +0 -203
- package/src/testing/suites/solveMetricSinkSuite.ts +0 -135
- package/src/testing/suites/storageProviderSuite.ts +0 -175
- package/src/testing/suites/userProfileStoreSuite.ts +0 -134
- package/src/userProfile/index.ts +0 -3
- package/src/userProfile/interface.ts +0 -48
- package/src/userProfile/types.ts +0 -24
- package/src/utils/audit.ts +0 -46
- package/src/utils/index.ts +0 -2
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter conformance suite for IPlatformProjectGrantStore.
|
|
3
|
-
*
|
|
4
|
-
* Grants are hard-deleted (no soft-delete column). Tests cover the round-trip,
|
|
5
|
-
* dedup on (projectId, granteeType, granteeId), and the cascade hooks
|
|
6
|
-
* (`deleteByProject` + `deleteByGranteeOrg`).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import type { IPlatformProjectGrantStore } from '../../platformProjects/interface.js';
|
|
11
|
-
import type { PlatformProjectGrant } from '../../platformProjects/types.js';
|
|
12
|
-
import { SYSTEM_CONTEXT } from '../../context.js';
|
|
13
|
-
import { makeUuid } from './helpers.js';
|
|
14
|
-
|
|
15
|
-
export interface PlatformProjectGrantStoreConformanceOptions {
|
|
16
|
-
name: string;
|
|
17
|
-
createStore: () => Promise<IPlatformProjectGrantStore> | IPlatformProjectGrantStore;
|
|
18
|
-
/**
|
|
19
|
-
* Optional hook for adapters with FK constraints to seed a `projects` row
|
|
20
|
-
* before the suite writes a grant that references it. Adapters without FKs
|
|
21
|
-
* (local JSON) can omit this.
|
|
22
|
-
*/
|
|
23
|
-
seedProject?: (projectId: string) => Promise<void>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function grant(overrides: Partial<PlatformProjectGrant> = {}): PlatformProjectGrant {
|
|
27
|
-
return {
|
|
28
|
-
id: overrides.id ?? makeUuid(),
|
|
29
|
-
projectId: overrides.projectId ?? makeUuid(),
|
|
30
|
-
granteeType: overrides.granteeType ?? 'org',
|
|
31
|
-
granteeId: overrides.granteeId ?? makeUuid(),
|
|
32
|
-
canSolve: overrides.canSolve ?? false,
|
|
33
|
-
createdBy: overrides.createdBy ?? makeUuid(),
|
|
34
|
-
createdAt: overrides.createdAt ?? new Date().toISOString()
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function runPlatformProjectGrantStoreConformance(
|
|
39
|
-
opts: PlatformProjectGrantStoreConformanceOptions
|
|
40
|
-
): void {
|
|
41
|
-
const { name, createStore, seedProject } = opts;
|
|
42
|
-
const seed = async (): Promise<string> => {
|
|
43
|
-
const id = makeUuid();
|
|
44
|
-
if (seedProject) await seedProject(id);
|
|
45
|
-
return id;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
describe(`IPlatformProjectGrantStore conformance: ${name}`, () => {
|
|
49
|
-
it('create + listByProject round-trips the grant', async () => {
|
|
50
|
-
const store = await createStore();
|
|
51
|
-
const projectId = await seed();
|
|
52
|
-
const g = grant({ projectId, granteeType: 'org', canSolve: true });
|
|
53
|
-
await store.create(SYSTEM_CONTEXT, g);
|
|
54
|
-
|
|
55
|
-
const got = await store.listByProject(SYSTEM_CONTEXT, projectId);
|
|
56
|
-
expect(got).toHaveLength(1);
|
|
57
|
-
expect(got[0].id).toBe(g.id);
|
|
58
|
-
expect(got[0].canSolve).toBe(true);
|
|
59
|
-
expect(got[0].granteeType).toBe('org');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('listByProject scopes to the requested project', async () => {
|
|
63
|
-
const store = await createStore();
|
|
64
|
-
const p1 = await seed();
|
|
65
|
-
const p2 = await seed();
|
|
66
|
-
await store.create(SYSTEM_CONTEXT, grant({ projectId: p1 }));
|
|
67
|
-
await store.create(SYSTEM_CONTEXT, grant({ projectId: p2 }));
|
|
68
|
-
|
|
69
|
-
const got = await store.listByProject(SYSTEM_CONTEXT, p1);
|
|
70
|
-
expect(got).toHaveLength(1);
|
|
71
|
-
expect(got[0].projectId).toBe(p1);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('create rejects duplicate (projectId, granteeType, granteeId)', async () => {
|
|
75
|
-
const store = await createStore();
|
|
76
|
-
const projectId = await seed();
|
|
77
|
-
const granteeId = makeUuid();
|
|
78
|
-
await store.create(SYSTEM_CONTEXT, grant({ projectId, granteeType: 'org', granteeId }));
|
|
79
|
-
await expect(
|
|
80
|
-
store.create(SYSTEM_CONTEXT, grant({ projectId, granteeType: 'org', granteeId }))
|
|
81
|
-
).rejects.toThrow();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('user and org grants for the same id coexist (different granteeType)', async () => {
|
|
85
|
-
const store = await createStore();
|
|
86
|
-
const projectId = await seed();
|
|
87
|
-
const sharedId = makeUuid();
|
|
88
|
-
await store.create(
|
|
89
|
-
SYSTEM_CONTEXT,
|
|
90
|
-
grant({ projectId, granteeType: 'org', granteeId: sharedId })
|
|
91
|
-
);
|
|
92
|
-
await store.create(
|
|
93
|
-
SYSTEM_CONTEXT,
|
|
94
|
-
grant({ projectId, granteeType: 'user', granteeId: sharedId })
|
|
95
|
-
);
|
|
96
|
-
const got = await store.listByProject(SYSTEM_CONTEXT, projectId);
|
|
97
|
-
expect(got).toHaveLength(2);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('delete removes the grant and listByProject no longer returns it', async () => {
|
|
101
|
-
const store = await createStore();
|
|
102
|
-
const projectId = await seed();
|
|
103
|
-
const g = grant({ projectId });
|
|
104
|
-
await store.create(SYSTEM_CONTEXT, g);
|
|
105
|
-
await store.delete(SYSTEM_CONTEXT, g.id);
|
|
106
|
-
|
|
107
|
-
const got = await store.listByProject(SYSTEM_CONTEXT, projectId);
|
|
108
|
-
expect(got).toHaveLength(0);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('delete on unknown id throws', async () => {
|
|
112
|
-
const store = await createStore();
|
|
113
|
-
await expect(store.delete(SYSTEM_CONTEXT, makeUuid())).rejects.toThrow();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('deleteByProject removes every grant for that project', async () => {
|
|
117
|
-
const store = await createStore();
|
|
118
|
-
const target = await seed();
|
|
119
|
-
const other = await seed();
|
|
120
|
-
await store.create(SYSTEM_CONTEXT, grant({ projectId: target }));
|
|
121
|
-
await store.create(SYSTEM_CONTEXT, grant({ projectId: target }));
|
|
122
|
-
await store.create(SYSTEM_CONTEXT, grant({ projectId: other }));
|
|
123
|
-
|
|
124
|
-
await store.deleteByProject(SYSTEM_CONTEXT, target);
|
|
125
|
-
|
|
126
|
-
expect(await store.listByProject(SYSTEM_CONTEXT, target)).toHaveLength(0);
|
|
127
|
-
expect(await store.listByProject(SYSTEM_CONTEXT, other)).toHaveLength(1);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('deleteByProject is a no-op when no grants exist', async () => {
|
|
131
|
-
const store = await createStore();
|
|
132
|
-
await expect(store.deleteByProject(SYSTEM_CONTEXT, makeUuid())).resolves.toBeUndefined();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('deleteByGranteeOrg removes only org grants matching the id, leaving user grants', async () => {
|
|
136
|
-
const store = await createStore();
|
|
137
|
-
const projectId = await seed();
|
|
138
|
-
const orgId = makeUuid();
|
|
139
|
-
const userId = makeUuid();
|
|
140
|
-
await store.create(
|
|
141
|
-
SYSTEM_CONTEXT,
|
|
142
|
-
grant({ projectId, granteeType: 'org', granteeId: orgId })
|
|
143
|
-
);
|
|
144
|
-
await store.create(
|
|
145
|
-
SYSTEM_CONTEXT,
|
|
146
|
-
grant({ projectId, granteeType: 'user', granteeId: userId })
|
|
147
|
-
);
|
|
148
|
-
// A user grant whose granteeId happens to equal the orgId — must NOT be touched.
|
|
149
|
-
await store.create(
|
|
150
|
-
SYSTEM_CONTEXT,
|
|
151
|
-
grant({ projectId, granteeType: 'user', granteeId: orgId })
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
await store.deleteByGranteeOrg(SYSTEM_CONTEXT, orgId);
|
|
155
|
-
|
|
156
|
-
const remaining = await store.listByProject(SYSTEM_CONTEXT, projectId);
|
|
157
|
-
expect(remaining).toHaveLength(2);
|
|
158
|
-
expect(remaining.every((g) => g.granteeType === 'user')).toBe(true);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('deleteByGranteeOrg is a no-op when no matching grants exist', async () => {
|
|
162
|
-
const store = await createStore();
|
|
163
|
-
await expect(store.deleteByGranteeOrg(SYSTEM_CONTEXT, makeUuid())).resolves.toBeUndefined();
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
}
|
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
// Conformance suite for IProjectStore: CRUD, membership, access checks across adapters.
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from 'vitest';
|
|
4
|
-
import type { IProjectStore } from '../../projects/interface.js';
|
|
5
|
-
import type { Project, ProjectMember } from '../../index.js';
|
|
6
|
-
import { makeSeedHelpers, makeUuid, noopSeedUser, type SeedUserFn } from './helpers.js';
|
|
7
|
-
|
|
8
|
-
export interface ProjectStoreConformanceOptions {
|
|
9
|
-
/** Name to show in test output (e.g. "local-provider"). */
|
|
10
|
-
name: string;
|
|
11
|
-
/**
|
|
12
|
-
* Factory that returns a fresh store, the orgId to use for projects, and
|
|
13
|
-
* the userId that owns the org (used to scope project ownership).
|
|
14
|
-
* In single-org mode, return the pre-existing org's id.
|
|
15
|
-
*
|
|
16
|
-
* `ownerSessionToken` is optional — adapters that enforce auth at the DB
|
|
17
|
-
* layer (Supabase RLS) must include it so `ctx(ownerId)` runs the call as
|
|
18
|
-
* that authenticated user. Local providers can omit it.
|
|
19
|
-
*/
|
|
20
|
-
createStore: () => Promise<{
|
|
21
|
-
store: IProjectStore;
|
|
22
|
-
orgId: string;
|
|
23
|
-
ownerId: string;
|
|
24
|
-
ownerSessionToken?: string;
|
|
25
|
-
}>;
|
|
26
|
-
/**
|
|
27
|
-
* Hook adapters with user FK constraints use to seed `auth.users` before
|
|
28
|
-
* a conformance test references a user id. Returns the id the adapter
|
|
29
|
-
* actually stored — callers use that, not the suggested one.
|
|
30
|
-
*/
|
|
31
|
-
seedUser?: SeedUserFn;
|
|
32
|
-
/** If true, run ctx-isolation tests (adapters with row-level security). */
|
|
33
|
-
ctxIsolation?: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function project(orgId: string, ownerId: string, overrides: Partial<Project> = {}): Project {
|
|
37
|
-
const now = new Date().toISOString();
|
|
38
|
-
return {
|
|
39
|
-
id: overrides.id ?? makeUuid(),
|
|
40
|
-
orgId,
|
|
41
|
-
name: overrides.name ?? 'Test Project',
|
|
42
|
-
slug: overrides.slug ?? `test-${Math.random().toString(36).slice(2, 8)}`,
|
|
43
|
-
visibility: overrides.visibility ?? 'private',
|
|
44
|
-
ownerId: overrides.ownerId ?? ownerId,
|
|
45
|
-
createdBy: overrides.createdBy ?? ownerId,
|
|
46
|
-
updatedBy: overrides.updatedBy ?? ownerId,
|
|
47
|
-
autoJoinOnUpload: overrides.autoJoinOnUpload ?? false,
|
|
48
|
-
createdAt: overrides.createdAt ?? now,
|
|
49
|
-
updatedAt: overrides.updatedAt ?? now,
|
|
50
|
-
deletedAt: overrides.deletedAt ?? null,
|
|
51
|
-
...overrides
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function member(projectId: string, userId: string, role: ProjectMember['role']): ProjectMember {
|
|
56
|
-
const now = new Date().toISOString();
|
|
57
|
-
return {
|
|
58
|
-
projectId,
|
|
59
|
-
userId,
|
|
60
|
-
role,
|
|
61
|
-
joinedAt: now,
|
|
62
|
-
updatedAt: now,
|
|
63
|
-
updatedBy: userId,
|
|
64
|
-
deletedAt: null
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function runProjectStoreConformance(opts: ProjectStoreConformanceOptions): void {
|
|
69
|
-
const { name, createStore: rawCreateStore, seedUser = noopSeedUser, ctxIsolation = false } = opts;
|
|
70
|
-
const { seed, ctx, registerToken } = makeSeedHelpers(seedUser);
|
|
71
|
-
const createStore = async () => {
|
|
72
|
-
const result = await rawCreateStore();
|
|
73
|
-
if (result.ownerSessionToken) registerToken(result.ownerId, result.ownerSessionToken);
|
|
74
|
-
return result;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
describe(`IProjectStore conformance: ${name}`, () => {
|
|
78
|
-
// ============================================================================
|
|
79
|
-
// Projects
|
|
80
|
-
// ============================================================================
|
|
81
|
-
|
|
82
|
-
it('createProject + getProject returns the project', async () => {
|
|
83
|
-
const { store, orgId, ownerId } = await createStore();
|
|
84
|
-
const p = project(orgId, ownerId);
|
|
85
|
-
await store.createProject(ctx(ownerId), p);
|
|
86
|
-
const got = await store.getProject(ctx(ownerId), p.id);
|
|
87
|
-
expect(got?.name).toBe('Test Project');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('getProject returns null for missing id', async () => {
|
|
91
|
-
const { store, ownerId } = await createStore();
|
|
92
|
-
const got = await store.getProject(ctx(ownerId), makeUuid());
|
|
93
|
-
expect(got).toBeNull();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('getProjectBySlug finds project by slug', async () => {
|
|
97
|
-
const { store, orgId, ownerId } = await createStore();
|
|
98
|
-
const p = project(orgId, ownerId, { slug: 'my-proj' });
|
|
99
|
-
await store.createProject(ctx(ownerId), p);
|
|
100
|
-
const got = await store.getProjectBySlug(ctx(ownerId), orgId, 'my-proj');
|
|
101
|
-
expect(got?.id).toBe(p.id);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('getProjectBySlug returns null for missing slug', async () => {
|
|
105
|
-
const { store, orgId, ownerId } = await createStore();
|
|
106
|
-
const got = await store.getProjectBySlug(ctx(ownerId), orgId, 'nonexistent');
|
|
107
|
-
expect(got).toBeNull();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('updateProject modifies name, slug, description, visibility', async () => {
|
|
111
|
-
const { store, orgId, ownerId } = await createStore();
|
|
112
|
-
const p = project(orgId, ownerId, { slug: 'old', visibility: 'private' });
|
|
113
|
-
await store.createProject(ctx(ownerId), p);
|
|
114
|
-
await store.updateProject(ctx(ownerId), p.id, {
|
|
115
|
-
name: 'New Name',
|
|
116
|
-
slug: 'new-slug',
|
|
117
|
-
description: 'New desc',
|
|
118
|
-
visibility: 'org'
|
|
119
|
-
});
|
|
120
|
-
const got = await store.getProject(ctx(ownerId), p.id);
|
|
121
|
-
expect(got?.name).toBe('New Name');
|
|
122
|
-
expect(got?.slug).toBe('new-slug');
|
|
123
|
-
expect(got?.description).toBe('New desc');
|
|
124
|
-
expect(got?.visibility).toBe('org');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('createProject rejects duplicate name in the same org (case-insensitive)', async () => {
|
|
128
|
-
const { store, orgId, ownerId } = await createStore();
|
|
129
|
-
await store.createProject(ctx(ownerId), project(orgId, ownerId, { name: 'Shared' }));
|
|
130
|
-
await expect(
|
|
131
|
-
store.createProject(ctx(ownerId), project(orgId, ownerId, { name: 'shared' }))
|
|
132
|
-
).rejects.toThrow();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('updateProject rejects renaming to a name already used in the same org', async () => {
|
|
136
|
-
const { store, orgId, ownerId } = await createStore();
|
|
137
|
-
const a = project(orgId, ownerId, { name: 'Alpha' });
|
|
138
|
-
const b = project(orgId, ownerId, { name: 'Beta' });
|
|
139
|
-
await store.createProject(ctx(ownerId), a);
|
|
140
|
-
await store.createProject(ctx(ownerId), b);
|
|
141
|
-
await expect(store.updateProject(ctx(ownerId), b.id, { name: 'ALPHA' })).rejects.toThrow();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('deleteProject removes the project', async () => {
|
|
145
|
-
const { store, orgId, ownerId } = await createStore();
|
|
146
|
-
const p = project(orgId, ownerId);
|
|
147
|
-
await store.createProject(ctx(ownerId), p);
|
|
148
|
-
await store.deleteProject(ctx(ownerId), p.id);
|
|
149
|
-
const got = await store.getProject(ctx(ownerId), p.id);
|
|
150
|
-
expect(got).toBeNull();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('listProjects returns all projects with pagination', async () => {
|
|
154
|
-
const { store, orgId, ownerId } = await createStore();
|
|
155
|
-
for (let i = 0; i < 3; i++) {
|
|
156
|
-
await store.createProject(
|
|
157
|
-
ctx(ownerId),
|
|
158
|
-
project(orgId, ownerId, { name: `Paginated ${i}` })
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
const page = await store.listProjects(ctx(ownerId), orgId, { limit: 2 });
|
|
162
|
-
expect(page.items.length).toBe(2);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// ============================================================================
|
|
166
|
-
// Project Members
|
|
167
|
-
// ============================================================================
|
|
168
|
-
|
|
169
|
-
it('addProjectMember + getProjectMember returns the member', async () => {
|
|
170
|
-
const { store, orgId, ownerId } = await createStore();
|
|
171
|
-
const p = project(orgId, ownerId);
|
|
172
|
-
await store.createProject(ctx(ownerId), p);
|
|
173
|
-
const u2 = await seed();
|
|
174
|
-
await store.addProjectMember(ctx(ownerId), member(p.id, u2, 'editor'));
|
|
175
|
-
const got = await store.getProjectMember(ctx(ownerId), p.id, u2);
|
|
176
|
-
expect(got?.role).toBe('editor');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('getProjectMember returns null for missing member', async () => {
|
|
180
|
-
const { store, orgId, ownerId } = await createStore();
|
|
181
|
-
const p = project(orgId, ownerId);
|
|
182
|
-
await store.createProject(ctx(ownerId), p);
|
|
183
|
-
const got = await store.getProjectMember(ctx(ownerId), p.id, makeUuid());
|
|
184
|
-
expect(got).toBeNull();
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('updateProjectMemberRole changes role', async () => {
|
|
188
|
-
const { store, orgId, ownerId } = await createStore();
|
|
189
|
-
const p = project(orgId, ownerId);
|
|
190
|
-
await store.createProject(ctx(ownerId), p);
|
|
191
|
-
const u2 = await seed();
|
|
192
|
-
await store.addProjectMember(ctx(ownerId), member(p.id, u2, 'viewer'));
|
|
193
|
-
await store.updateProjectMemberRole(ctx(ownerId), p.id, u2, 'editor');
|
|
194
|
-
const got = await store.getProjectMember(ctx(ownerId), p.id, u2);
|
|
195
|
-
expect(got?.role).toBe('editor');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('removeProjectMember deletes the member', async () => {
|
|
199
|
-
const { store, orgId, ownerId } = await createStore();
|
|
200
|
-
const p = project(orgId, ownerId);
|
|
201
|
-
await store.createProject(ctx(ownerId), p);
|
|
202
|
-
const u2 = await seed();
|
|
203
|
-
await store.addProjectMember(ctx(ownerId), member(p.id, u2, 'editor'));
|
|
204
|
-
await store.removeProjectMember(ctx(ownerId), p.id, u2);
|
|
205
|
-
const got = await store.getProjectMember(ctx(ownerId), p.id, u2);
|
|
206
|
-
expect(got).toBeNull();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('listProjectMembers returns members with pagination', async () => {
|
|
210
|
-
const { store, orgId, ownerId } = await createStore();
|
|
211
|
-
const p = project(orgId, ownerId);
|
|
212
|
-
await store.createProject(ctx(ownerId), p);
|
|
213
|
-
for (let i = 0; i < 3; i++) {
|
|
214
|
-
const u = await seed();
|
|
215
|
-
await store.addProjectMember(ctx(ownerId), member(p.id, u, 'viewer'));
|
|
216
|
-
}
|
|
217
|
-
const page = await store.listProjectMembers(ctx(ownerId), p.id, { limit: 2 });
|
|
218
|
-
expect(page.items.length).toBe(2);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
// ============================================================================
|
|
222
|
-
// Audit fields + soft delete
|
|
223
|
-
// ============================================================================
|
|
224
|
-
|
|
225
|
-
it('createProject populates createdBy and updatedBy', async () => {
|
|
226
|
-
const { store, orgId, ownerId } = await createStore();
|
|
227
|
-
const p = project(orgId, ownerId);
|
|
228
|
-
await store.createProject(ctx(ownerId), p);
|
|
229
|
-
const got = await store.getProject(ctx(ownerId), p.id);
|
|
230
|
-
expect(got?.createdBy).toBe(ownerId);
|
|
231
|
-
expect(got?.updatedBy).toBe(ownerId);
|
|
232
|
-
expect(got?.deletedAt ?? null).toBeNull();
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('updateProject advances updatedBy to the caller, preserves createdBy', async () => {
|
|
236
|
-
const { store, orgId, ownerId } = await createStore();
|
|
237
|
-
const p = project(orgId, ownerId);
|
|
238
|
-
await store.createProject(ctx(ownerId), p);
|
|
239
|
-
const editor = await seed();
|
|
240
|
-
// Give the editor membership so the adapter's auth check (if any) permits
|
|
241
|
-
// the update. Conformance runs with permissive ctx defaults in practice.
|
|
242
|
-
await store.addProjectMember(ctx(ownerId), member(p.id, editor, 'editor'));
|
|
243
|
-
await store.updateProject(ctx(editor), p.id, { name: 'Renamed' });
|
|
244
|
-
const got = await store.getProject(ctx(ownerId), p.id);
|
|
245
|
-
expect(got?.updatedBy).toBe(editor);
|
|
246
|
-
expect(got?.createdBy).toBe(ownerId);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('deleteProject soft-deletes — the project stops appearing in reads', async () => {
|
|
250
|
-
const { store, orgId, ownerId } = await createStore();
|
|
251
|
-
const p = project(orgId, ownerId);
|
|
252
|
-
await store.createProject(ctx(ownerId), p);
|
|
253
|
-
await store.deleteProject(ctx(ownerId), p.id);
|
|
254
|
-
expect(await store.getProject(ctx(ownerId), p.id)).toBeNull();
|
|
255
|
-
expect(await store.getProjectBySlug(ctx(ownerId), orgId, p.slug)).toBeNull();
|
|
256
|
-
const listed = await store.listProjects(ctx(ownerId), orgId, { limit: 100 });
|
|
257
|
-
expect(listed.items.map((x) => x.id)).not.toContain(p.id);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('deleteProject cascades soft-delete to project members', async () => {
|
|
261
|
-
const { store, orgId, ownerId } = await createStore();
|
|
262
|
-
const p = project(orgId, ownerId);
|
|
263
|
-
await store.createProject(ctx(ownerId), p);
|
|
264
|
-
const u2 = await seed();
|
|
265
|
-
await store.addProjectMember(ctx(ownerId), member(p.id, u2, 'editor'));
|
|
266
|
-
await store.deleteProject(ctx(ownerId), p.id);
|
|
267
|
-
const got = await store.getProjectMember(ctx(ownerId), p.id, u2);
|
|
268
|
-
expect(got).toBeNull();
|
|
269
|
-
const listed = await store.listProjectMembers(ctx(ownerId), p.id, { limit: 100 });
|
|
270
|
-
expect(listed.items).toHaveLength(0);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('removeProjectMember soft-deletes — the member stops appearing in reads', async () => {
|
|
274
|
-
const { store, orgId, ownerId } = await createStore();
|
|
275
|
-
const p = project(orgId, ownerId);
|
|
276
|
-
await store.createProject(ctx(ownerId), p);
|
|
277
|
-
const u2 = await seed();
|
|
278
|
-
await store.addProjectMember(ctx(ownerId), member(p.id, u2, 'editor'));
|
|
279
|
-
await store.removeProjectMember(ctx(ownerId), p.id, u2);
|
|
280
|
-
expect(await store.getProjectMember(ctx(ownerId), p.id, u2)).toBeNull();
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// ============================================================================
|
|
284
|
-
// reactivateProject — un-delete (slug-tombstone problem)
|
|
285
|
-
// ============================================================================
|
|
286
|
-
|
|
287
|
-
it('reactivateProject returns null when no tombstone exists for that slug', async () => {
|
|
288
|
-
const { store, orgId, ownerId } = await createStore();
|
|
289
|
-
const got = await store.reactivateProject(ctx(ownerId), orgId, 'no-such-slug');
|
|
290
|
-
expect(got).toBeNull();
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('reactivateProject clears the tombstone and surfaces it via getProject/getProjectBySlug', async () => {
|
|
294
|
-
const { store, orgId, ownerId } = await createStore();
|
|
295
|
-
const p = project(orgId, ownerId, { slug: 'revive-me' });
|
|
296
|
-
await store.createProject(ctx(ownerId), p);
|
|
297
|
-
await store.deleteProject(ctx(ownerId), p.id);
|
|
298
|
-
// Before reactivation: invisible
|
|
299
|
-
expect(await store.getProject(ctx(ownerId), p.id)).toBeNull();
|
|
300
|
-
expect(await store.getProjectBySlug(ctx(ownerId), orgId, 'revive-me')).toBeNull();
|
|
301
|
-
|
|
302
|
-
const revived = await store.reactivateProject(ctx(ownerId), orgId, 'revive-me');
|
|
303
|
-
expect(revived?.id).toBe(p.id);
|
|
304
|
-
expect(revived?.slug).toBe('revive-me');
|
|
305
|
-
// After reactivation: visible again
|
|
306
|
-
expect(await store.getProject(ctx(ownerId), p.id)).not.toBeNull();
|
|
307
|
-
expect(await store.getProjectBySlug(ctx(ownerId), orgId, 'revive-me')).not.toBeNull();
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('reactivateProject restores the owner project_member row', async () => {
|
|
311
|
-
const { store, orgId, ownerId } = await createStore();
|
|
312
|
-
const p = project(orgId, ownerId, { slug: 'member-revive' });
|
|
313
|
-
await store.createProject(ctx(ownerId), p);
|
|
314
|
-
await store.deleteProject(ctx(ownerId), p.id);
|
|
315
|
-
await store.reactivateProject(ctx(ownerId), orgId, 'member-revive');
|
|
316
|
-
const m = await store.getProjectMember(ctx(ownerId), p.id, ownerId);
|
|
317
|
-
expect(m?.role).toBe('owner');
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it('createProject after delete+reactivate on the same slug works', async () => {
|
|
321
|
-
// This covers the bootstrap re-seed scenario: delete project, then createProject
|
|
322
|
-
// with the same slug succeeds after reactivating the tombstone.
|
|
323
|
-
const { store, orgId, ownerId } = await createStore();
|
|
324
|
-
const p = project(orgId, ownerId, { slug: 'reseed-slug' });
|
|
325
|
-
await store.createProject(ctx(ownerId), p);
|
|
326
|
-
await store.deleteProject(ctx(ownerId), p.id);
|
|
327
|
-
await store.reactivateProject(ctx(ownerId), orgId, 'reseed-slug');
|
|
328
|
-
// Now it's live; a second delete + fresh createProject should work.
|
|
329
|
-
await store.deleteProject(ctx(ownerId), p.id);
|
|
330
|
-
await store.reactivateProject(ctx(ownerId), orgId, 'reseed-slug');
|
|
331
|
-
expect(await store.getProjectBySlug(ctx(ownerId), orgId, 'reseed-slug')).not.toBeNull();
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
// ============================================================================
|
|
335
|
-
// Project flags (autoJoinOnUpload)
|
|
336
|
-
// ============================================================================
|
|
337
|
-
|
|
338
|
-
it('create + get round-trips autoJoinOnUpload', async () => {
|
|
339
|
-
const { store, orgId, ownerId } = await createStore();
|
|
340
|
-
const p = project(orgId, ownerId, {
|
|
341
|
-
visibility: 'public',
|
|
342
|
-
autoJoinOnUpload: true
|
|
343
|
-
});
|
|
344
|
-
await store.createProject(ctx(ownerId), p);
|
|
345
|
-
const got = await store.getProject(ctx(ownerId), p.id);
|
|
346
|
-
expect(got?.autoJoinOnUpload).toBe(true);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it('flag defaults to false on a project created without it set', async () => {
|
|
350
|
-
const { store, orgId, ownerId } = await createStore();
|
|
351
|
-
const p = project(orgId, ownerId);
|
|
352
|
-
await store.createProject(ctx(ownerId), p);
|
|
353
|
-
const got = await store.getProject(ctx(ownerId), p.id);
|
|
354
|
-
expect(got?.autoJoinOnUpload).toBe(false);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
if (ctxIsolation) {
|
|
358
|
-
it('ctx isolation: projects created by one user are not visible to another', async () => {
|
|
359
|
-
const { store, orgId } = await createStore();
|
|
360
|
-
const userA = await seed();
|
|
361
|
-
const userB = await seed();
|
|
362
|
-
const pA = project(orgId, userA);
|
|
363
|
-
await store.createProject(ctx(userA), pA);
|
|
364
|
-
|
|
365
|
-
const page = await store.listProjects(ctx(userB), orgId);
|
|
366
|
-
expect(page.items.map((p) => p.id)).not.toContain(pA.id);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it('ctx isolation: user cannot get a project they do not own', async () => {
|
|
370
|
-
const { store, orgId } = await createStore();
|
|
371
|
-
const userA = await seed();
|
|
372
|
-
const userB = await seed();
|
|
373
|
-
const pA = project(orgId, userA);
|
|
374
|
-
await store.createProject(ctx(userA), pA);
|
|
375
|
-
|
|
376
|
-
const got = await store.getProject(ctx(userB), pA.id);
|
|
377
|
-
expect(got).toBeNull();
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
}
|