@selvajs/local-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 +123 -0
- package/dist/auth/LocalAuthProvider.d.ts +28 -0
- package/dist/auth/LocalAuthProvider.d.ts.map +1 -0
- package/dist/auth/LocalAuthProvider.js +142 -0
- package/dist/auth/LocalAuthProvider.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 +36 -0
- package/dist/auth/__tests__/conformance.test.js.map +1 -0
- package/dist/auth/hmac.d.ts +18 -0
- package/dist/auth/hmac.d.ts.map +1 -0
- package/dist/auth/hmac.js +41 -0
- package/dist/auth/hmac.js.map +1 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +4 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/users.d.ts +38 -0
- package/dist/auth/users.d.ts.map +1 -0
- package/dist/auth/users.js +100 -0
- package/dist/auth/users.js.map +1 -0
- package/dist/compute/FilesystemComputeProvider.d.ts +16 -0
- package/dist/compute/FilesystemComputeProvider.d.ts.map +1 -0
- package/dist/compute/FilesystemComputeProvider.js +51 -0
- package/dist/compute/FilesystemComputeProvider.js.map +1 -0
- package/dist/compute/SingleComputeServerProvider.d.ts +15 -0
- package/dist/compute/SingleComputeServerProvider.d.ts.map +1 -0
- package/dist/compute/SingleComputeServerProvider.js +26 -0
- package/dist/compute/SingleComputeServerProvider.js.map +1 -0
- package/dist/compute/types.d.ts +30 -0
- package/dist/compute/types.d.ts.map +1 -0
- package/dist/compute/types.js +2 -0
- package/dist/compute/types.js.map +1 -0
- package/dist/computeServer/FilesystemComputeServerStore.d.ts +14 -0
- package/dist/computeServer/FilesystemComputeServerStore.d.ts.map +1 -0
- package/dist/computeServer/FilesystemComputeServerStore.js +35 -0
- package/dist/computeServer/FilesystemComputeServerStore.js.map +1 -0
- package/dist/computeServer/LocalComputeServerProvider.d.ts +18 -0
- package/dist/computeServer/LocalComputeServerProvider.d.ts.map +1 -0
- package/dist/computeServer/LocalComputeServerProvider.js +29 -0
- package/dist/computeServer/LocalComputeServerProvider.js.map +1 -0
- package/dist/computeServer/__tests__/conformance.test.d.ts +2 -0
- package/dist/computeServer/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/computeServer/__tests__/conformance.test.js +20 -0
- package/dist/computeServer/__tests__/conformance.test.js.map +1 -0
- package/dist/computeServer/index.d.ts +2 -0
- package/dist/computeServer/index.d.ts.map +1 -0
- package/dist/computeServer/index.js +2 -0
- package/dist/computeServer/index.js.map +1 -0
- package/dist/data/LocalComputeServerStore.d.ts +72 -0
- package/dist/data/LocalComputeServerStore.d.ts.map +1 -0
- package/dist/data/LocalComputeServerStore.js +207 -0
- package/dist/data/LocalComputeServerStore.js.map +1 -0
- package/dist/data/LocalDataProvider.d.ts +47 -0
- package/dist/data/LocalDataProvider.d.ts.map +1 -0
- package/dist/data/LocalDataProvider.js +118 -0
- package/dist/data/LocalDataProvider.js.map +1 -0
- package/dist/data/LocalDefinitionMetaProvider.d.ts +22 -0
- package/dist/data/LocalDefinitionMetaProvider.d.ts.map +1 -0
- package/dist/data/LocalDefinitionMetaProvider.js +131 -0
- package/dist/data/LocalDefinitionMetaProvider.js.map +1 -0
- package/dist/data/LocalDefinitionStore.d.ts +33 -0
- package/dist/data/LocalDefinitionStore.d.ts.map +1 -0
- package/dist/data/LocalDefinitionStore.js +274 -0
- package/dist/data/LocalDefinitionStore.js.map +1 -0
- package/dist/data/LocalInviteStore.d.ts +23 -0
- package/dist/data/LocalInviteStore.d.ts.map +1 -0
- package/dist/data/LocalInviteStore.js +98 -0
- package/dist/data/LocalInviteStore.js.map +1 -0
- package/dist/data/LocalOrgStore.d.ts +67 -0
- package/dist/data/LocalOrgStore.d.ts.map +1 -0
- package/dist/data/LocalOrgStore.js +255 -0
- package/dist/data/LocalOrgStore.js.map +1 -0
- package/dist/data/LocalPlatformProjectGrantStore.d.ts +14 -0
- package/dist/data/LocalPlatformProjectGrantStore.d.ts.map +1 -0
- package/dist/data/LocalPlatformProjectGrantStore.js +62 -0
- package/dist/data/LocalPlatformProjectGrantStore.js.map +1 -0
- package/dist/data/LocalProjectStore.d.ts +30 -0
- package/dist/data/LocalProjectStore.d.ts.map +1 -0
- package/dist/data/LocalProjectStore.js +171 -0
- package/dist/data/LocalProjectStore.js.map +1 -0
- package/dist/data/LocalShareLinkStore.d.ts +39 -0
- package/dist/data/LocalShareLinkStore.d.ts.map +1 -0
- package/dist/data/LocalShareLinkStore.js +108 -0
- package/dist/data/LocalShareLinkStore.js.map +1 -0
- package/dist/data/__tests__/LocalDefinitionMetaProvider.test.d.ts +2 -0
- package/dist/data/__tests__/LocalDefinitionMetaProvider.test.d.ts.map +1 -0
- package/dist/data/__tests__/LocalDefinitionMetaProvider.test.js +21 -0
- package/dist/data/__tests__/LocalDefinitionMetaProvider.test.js.map +1 -0
- package/dist/data/__tests__/cascade.test.d.ts +2 -0
- package/dist/data/__tests__/cascade.test.d.ts.map +1 -0
- package/dist/data/__tests__/cascade.test.js +265 -0
- package/dist/data/__tests__/cascade.test.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 +21 -0
- package/dist/data/__tests__/compute-server-conformance.test.js.map +1 -0
- package/dist/data/__tests__/compute-server-encryption.test.d.ts +2 -0
- package/dist/data/__tests__/compute-server-encryption.test.d.ts.map +1 -0
- package/dist/data/__tests__/compute-server-encryption.test.js +131 -0
- package/dist/data/__tests__/compute-server-encryption.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 +20 -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 +24 -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 +21 -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 +36 -0
- package/dist/data/__tests__/org-conformance.test.js.map +1 -0
- package/dist/data/__tests__/platform-project-grant-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/platform-project-grant-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/platform-project-grant-conformance.test.js +20 -0
- package/dist/data/__tests__/platform-project-grant-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 +53 -0
- package/dist/data/__tests__/project-conformance.test.js.map +1 -0
- package/dist/data/__tests__/rules.test.d.ts +2 -0
- package/dist/data/__tests__/rules.test.d.ts.map +1 -0
- package/dist/data/__tests__/rules.test.js +484 -0
- package/dist/data/__tests__/rules.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 +20 -0
- package/dist/data/__tests__/share-link-conformance.test.js.map +1 -0
- package/dist/data/fsJson.d.ts +12 -0
- package/dist/data/fsJson.d.ts.map +1 -0
- package/dist/data/fsJson.js +29 -0
- package/dist/data/fsJson.js.map +1 -0
- package/dist/data/index.d.ts +13 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +9 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/pagination.d.ts +15 -0
- package/dist/data/pagination.d.ts.map +1 -0
- package/dist/data/pagination.js +36 -0
- package/dist/data/pagination.js.map +1 -0
- package/dist/data/secretCrypto.d.ts +23 -0
- package/dist/data/secretCrypto.d.ts.map +1 -0
- package/dist/data/secretCrypto.js +64 -0
- package/dist/data/secretCrypto.js.map +1 -0
- package/dist/data/userData.d.ts +40 -0
- package/dist/data/userData.d.ts.map +1 -0
- package/dist/data/userData.js +84 -0
- package/dist/data/userData.js.map +1 -0
- package/dist/definitions/LocalDefinitionMetaProvider.d.ts +27 -0
- package/dist/definitions/LocalDefinitionMetaProvider.d.ts.map +1 -0
- package/dist/definitions/LocalDefinitionMetaProvider.js +188 -0
- package/dist/definitions/LocalDefinitionMetaProvider.js.map +1 -0
- package/dist/definitions/__tests__/conformance.test.d.ts +2 -0
- package/dist/definitions/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/definitions/__tests__/conformance.test.js +20 -0
- package/dist/definitions/__tests__/conformance.test.js.map +1 -0
- package/dist/definitions/index.d.ts +2 -0
- package/dist/definitions/index.d.ts.map +1 -0
- package/dist/definitions/index.js +2 -0
- package/dist/definitions/index.js.map +1 -0
- package/dist/definitions/providers/filesystem-files.d.ts +24 -0
- package/dist/definitions/providers/filesystem-files.d.ts.map +1 -0
- package/dist/definitions/providers/filesystem-files.js +170 -0
- package/dist/definitions/providers/filesystem-files.js.map +1 -0
- package/dist/definitions/providers/filesystem-meta.d.ts +17 -0
- package/dist/definitions/providers/filesystem-meta.d.ts.map +1 -0
- package/dist/definitions/providers/filesystem-meta.js +216 -0
- package/dist/definitions/providers/filesystem-meta.js.map +1 -0
- package/dist/fsJson.d.ts +12 -0
- package/dist/fsJson.d.ts.map +1 -0
- package/dist/fsJson.js +29 -0
- package/dist/fsJson.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/invites/LocalInviteProvider.d.ts +24 -0
- package/dist/invites/LocalInviteProvider.d.ts.map +1 -0
- package/dist/invites/LocalInviteProvider.js +89 -0
- package/dist/invites/LocalInviteProvider.js.map +1 -0
- package/dist/invites/__tests__/conformance.test.d.ts +2 -0
- package/dist/invites/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/invites/__tests__/conformance.test.js +21 -0
- package/dist/invites/__tests__/conformance.test.js.map +1 -0
- package/dist/organizations/LocalOrganizationProvider.d.ts +41 -0
- package/dist/organizations/LocalOrganizationProvider.d.ts.map +1 -0
- package/dist/organizations/LocalOrganizationProvider.js +198 -0
- package/dist/organizations/LocalOrganizationProvider.js.map +1 -0
- package/dist/organizations/__tests__/conformance.test.d.ts +2 -0
- package/dist/organizations/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/organizations/__tests__/conformance.test.js +20 -0
- package/dist/organizations/__tests__/conformance.test.js.map +1 -0
- package/dist/organizations/index.d.ts +2 -0
- package/dist/organizations/index.d.ts.map +1 -0
- package/dist/organizations/index.js +2 -0
- package/dist/organizations/index.js.map +1 -0
- package/dist/pagination.d.ts +15 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +36 -0
- package/dist/pagination.js.map +1 -0
- package/dist/permissions/LocalPlatformPermissionStore.d.ts +39 -0
- package/dist/permissions/LocalPlatformPermissionStore.d.ts.map +1 -0
- package/dist/permissions/LocalPlatformPermissionStore.js +117 -0
- package/dist/permissions/LocalPlatformPermissionStore.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 +37 -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/projects/LocalProjectProvider.d.ts +21 -0
- package/dist/projects/LocalProjectProvider.d.ts.map +1 -0
- package/dist/projects/LocalProjectProvider.js +125 -0
- package/dist/projects/LocalProjectProvider.js.map +1 -0
- package/dist/projects/__tests__/conformance.test.d.ts +2 -0
- package/dist/projects/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/projects/__tests__/conformance.test.js +44 -0
- package/dist/projects/__tests__/conformance.test.js.map +1 -0
- package/dist/projects/index.d.ts +2 -0
- package/dist/projects/index.d.ts.map +1 -0
- package/dist/projects/index.js +2 -0
- package/dist/projects/index.js.map +1 -0
- package/dist/storage/LocalStorageProvider.d.ts +27 -0
- package/dist/storage/LocalStorageProvider.d.ts.map +1 -0
- package/dist/storage/LocalStorageProvider.js +74 -0
- package/dist/storage/LocalStorageProvider.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 +20 -0
- package/dist/storage/__tests__/conformance.test.js.map +1 -0
- package/dist/storage/index.d.ts +2 -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/LocalUserProfileProvider.d.ts +25 -0
- package/dist/userProfile/LocalUserProfileProvider.d.ts.map +1 -0
- package/dist/userProfile/LocalUserProfileProvider.js +110 -0
- package/dist/userProfile/LocalUserProfileProvider.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 +40 -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 +70 -0
- package/src/README.md +37 -0
- package/src/auth/LocalAuthProvider.ts +165 -0
- package/src/auth/__tests__/conformance.test.ts +40 -0
- package/src/auth/hmac.ts +53 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/users.ts +151 -0
- package/src/data/LocalComputeServerStore.ts +290 -0
- package/src/data/LocalDataProvider.ts +148 -0
- package/src/data/LocalDefinitionStore.ts +369 -0
- package/src/data/LocalInviteStore.ts +117 -0
- package/src/data/LocalOrgStore.ts +356 -0
- package/src/data/LocalPlatformProjectGrantStore.ts +85 -0
- package/src/data/LocalProjectStore.ts +274 -0
- package/src/data/LocalShareLinkStore.ts +138 -0
- package/src/data/__tests__/cascade.test.ts +300 -0
- package/src/data/__tests__/compute-server-conformance.test.ts +26 -0
- package/src/data/__tests__/compute-server-encryption.test.ts +185 -0
- package/src/data/__tests__/definition-conformance.test.ts +23 -0
- package/src/data/__tests__/event-sink-conformance.test.ts +28 -0
- package/src/data/__tests__/invite-conformance.test.ts +24 -0
- package/src/data/__tests__/org-conformance.test.ts +43 -0
- package/src/data/__tests__/platform-project-grant-conformance.test.ts +24 -0
- package/src/data/__tests__/project-conformance.test.ts +64 -0
- package/src/data/__tests__/rules.test.ts +682 -0
- package/src/data/__tests__/share-link-conformance.test.ts +23 -0
- package/src/data/fsJson.ts +28 -0
- package/src/data/index.ts +16 -0
- package/src/data/pagination.ts +48 -0
- package/src/data/secretCrypto.ts +69 -0
- package/src/data/userData.ts +134 -0
- package/src/index.ts +42 -0
- package/src/permissions/LocalPlatformPermissionStore.ts +129 -0
- package/src/permissions/__tests__/conformance.test.ts +40 -0
- package/src/permissions/index.ts +1 -0
- package/src/storage/LocalStorageProvider.ts +78 -0
- package/src/storage/__tests__/conformance.test.ts +23 -0
- package/src/storage/index.ts +1 -0
- package/src/userProfile/LocalUserProfileProvider.ts +135 -0
- package/src/userProfile/__tests__/conformance.test.ts +43 -0
- package/src/userProfile/index.ts +1 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import type {
|
|
3
|
+
IDefinitionStore,
|
|
4
|
+
IProjectStore,
|
|
5
|
+
IEventSink,
|
|
6
|
+
DefinitionRecord,
|
|
7
|
+
DefinitionRecordPatch,
|
|
8
|
+
DefinitionVersion,
|
|
9
|
+
RequestContext,
|
|
10
|
+
DefinitionListOptions,
|
|
11
|
+
ListOptions,
|
|
12
|
+
Page
|
|
13
|
+
} from '@selvajs/platform';
|
|
14
|
+
import {
|
|
15
|
+
ProviderError,
|
|
16
|
+
auditUpdate,
|
|
17
|
+
auditSoftDelete,
|
|
18
|
+
actorFrom,
|
|
19
|
+
NoopEventSink
|
|
20
|
+
} from '@selvajs/platform';
|
|
21
|
+
import { paginate, applyOrder } from './pagination.js';
|
|
22
|
+
import { readJsonFile, writeJsonFile } from './fsJson.js';
|
|
23
|
+
|
|
24
|
+
interface DefinitionsConfig {
|
|
25
|
+
definitions: Record<string, DefinitionRecord>;
|
|
26
|
+
definitionVersions: Record<string, DefinitionVersion>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Always return a fresh object — `readJsonFile` returns its fallback by
|
|
30
|
+
* reference when the file is missing, so a shared singleton would let one
|
|
31
|
+
* read pollute the next. */
|
|
32
|
+
const empty = (): DefinitionsConfig => ({ definitions: {}, definitionVersions: {} });
|
|
33
|
+
|
|
34
|
+
export class LocalDefinitionStore implements IDefinitionStore {
|
|
35
|
+
private readonly configPath: string;
|
|
36
|
+
private readonly events: IEventSink;
|
|
37
|
+
private projectProvider?: IProjectStore;
|
|
38
|
+
|
|
39
|
+
static fromEnv(env: Record<string, string | undefined>): LocalDefinitionStore {
|
|
40
|
+
if (!env.DATA_PATH) throw new Error('Missing required env var: DATA_PATH');
|
|
41
|
+
return new LocalDefinitionStore(env.DATA_PATH);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
definitionsPath: string,
|
|
46
|
+
projectProvider?: IProjectStore,
|
|
47
|
+
events: IEventSink = new NoopEventSink()
|
|
48
|
+
) {
|
|
49
|
+
this.configPath = path.join(definitionsPath, 'definitions-config.json');
|
|
50
|
+
this.projectProvider = projectProvider;
|
|
51
|
+
this.events = events;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setProjectProvider(projectProvider: IProjectStore): void {
|
|
55
|
+
this.projectProvider = projectProvider;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async readConfig(): Promise<DefinitionsConfig> {
|
|
59
|
+
return readJsonFile<DefinitionsConfig>(this.configPath, empty());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private live(record: DefinitionRecord | undefined | null): record is DefinitionRecord {
|
|
63
|
+
return Boolean(record && record.deletedAt == null);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async writeConfig(config: DefinitionsConfig): Promise<void> {
|
|
67
|
+
await writeJsonFile(this.configPath, config);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private sortedRecords(
|
|
71
|
+
records: DefinitionRecord[],
|
|
72
|
+
opts?: DefinitionListOptions
|
|
73
|
+
): DefinitionRecord[] {
|
|
74
|
+
const defaulted: DefinitionListOptions = {
|
|
75
|
+
...opts,
|
|
76
|
+
orderBy: opts?.orderBy ?? 'name',
|
|
77
|
+
orderDir: opts?.orderDir ?? 'asc'
|
|
78
|
+
};
|
|
79
|
+
return applyOrder([...records], defaulted, (r, field) => {
|
|
80
|
+
if (field === 'name') return r.displayName.toLowerCase();
|
|
81
|
+
if (field === 'solveCount') return r.solveCount ?? 0;
|
|
82
|
+
return (r as unknown as Record<string, unknown>)[field];
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private visibleRecords(
|
|
87
|
+
records: DefinitionRecord[],
|
|
88
|
+
opts?: DefinitionListOptions
|
|
89
|
+
): DefinitionRecord[] {
|
|
90
|
+
const filtered = records.filter((r) => r?.displayName && this.live(r));
|
|
91
|
+
if (opts?.statuses?.length) {
|
|
92
|
+
const allowed = new Set(opts.statuses);
|
|
93
|
+
return filtered.filter((r) => allowed.has(r.status));
|
|
94
|
+
}
|
|
95
|
+
return filtered.filter((r) => {
|
|
96
|
+
if (r.status === 'pending' && !opts?.includePending) return false;
|
|
97
|
+
if (r.status === 'archived' && !opts?.includeArchived) return false;
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async list(_ctx: RequestContext, opts?: DefinitionListOptions): Promise<Page<DefinitionRecord>> {
|
|
103
|
+
const config = await this.readConfig();
|
|
104
|
+
const records = this.visibleRecords(Object.values(config.definitions), opts);
|
|
105
|
+
return paginate(this.sortedRecords(records, opts), opts);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async listByProject(
|
|
109
|
+
_ctx: RequestContext,
|
|
110
|
+
projectId: string,
|
|
111
|
+
opts?: DefinitionListOptions
|
|
112
|
+
): Promise<Page<DefinitionRecord>> {
|
|
113
|
+
const config = await this.readConfig();
|
|
114
|
+
const records = this.visibleRecords(
|
|
115
|
+
Object.values(config.definitions).filter((r) => r?.projectId === projectId),
|
|
116
|
+
opts
|
|
117
|
+
);
|
|
118
|
+
return paginate(this.sortedRecords(records, opts), opts);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async listPublic(
|
|
122
|
+
ctx: RequestContext,
|
|
123
|
+
opts?: DefinitionListOptions & { orgId?: string }
|
|
124
|
+
): Promise<Page<DefinitionRecord>> {
|
|
125
|
+
if (!this.projectProvider) {
|
|
126
|
+
// Pre-wiring fallback: behave as if the default project is public, which
|
|
127
|
+
// matches the local bootstrap. Once setProjectProvider is called this
|
|
128
|
+
// branch stops executing.
|
|
129
|
+
return this.list(ctx, opts);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const config = await this.readConfig();
|
|
133
|
+
const records = Object.values(config.definitions).filter((r): r is DefinitionRecord =>
|
|
134
|
+
Boolean(r?.displayName && this.live(r))
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const projectIds = Array.from(new Set(records.map((r) => r.projectId)));
|
|
138
|
+
const projects = await Promise.all(
|
|
139
|
+
projectIds.map((id) => this.projectProvider!.getProject(ctx, id))
|
|
140
|
+
);
|
|
141
|
+
const publicProjectIds = new Set(
|
|
142
|
+
projects
|
|
143
|
+
.filter(
|
|
144
|
+
(p): p is NonNullable<typeof p> =>
|
|
145
|
+
p !== null && p.visibility === 'public' && (!opts?.orgId || p.orgId === opts.orgId)
|
|
146
|
+
)
|
|
147
|
+
.map((p) => p.id)
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const publicRecords = this.visibleRecords(
|
|
151
|
+
records.filter((r) => publicProjectIds.has(r.projectId)),
|
|
152
|
+
opts
|
|
153
|
+
);
|
|
154
|
+
return paginate(this.sortedRecords(publicRecords, opts), opts);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async get(_ctx: RequestContext, guid: string): Promise<DefinitionRecord | null> {
|
|
158
|
+
const config = await this.readConfig();
|
|
159
|
+
const r = config.definitions[guid];
|
|
160
|
+
return this.live(r) ? r : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async create(ctx: RequestContext, record: DefinitionRecord): Promise<void> {
|
|
164
|
+
const config = await this.readConfig();
|
|
165
|
+
const actor = ctx.userId || record.ownerId;
|
|
166
|
+
config.definitions[record.guid] = {
|
|
167
|
+
...record,
|
|
168
|
+
createdBy: record.createdBy || actor,
|
|
169
|
+
updatedBy: record.updatedBy || actor,
|
|
170
|
+
liveVersionId: record.liveVersionId ?? null,
|
|
171
|
+
draftVersionId: record.draftVersionId ?? null,
|
|
172
|
+
deletedAt: null
|
|
173
|
+
};
|
|
174
|
+
await this.writeConfig(config);
|
|
175
|
+
await this.events.emit({
|
|
176
|
+
type: 'definition.created',
|
|
177
|
+
definitionId: record.guid,
|
|
178
|
+
projectId: record.projectId,
|
|
179
|
+
actorId: actorFrom(ctx)
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async update(ctx: RequestContext, guid: string, patch: DefinitionRecordPatch): Promise<void> {
|
|
184
|
+
const config = await this.readConfig();
|
|
185
|
+
const existing = config.definitions[guid];
|
|
186
|
+
if (!this.live(existing)) throw new ProviderError(`Definition '${guid}' not found`, 404);
|
|
187
|
+
|
|
188
|
+
const clearable = (v: unknown) => (v === null ? undefined : v);
|
|
189
|
+
config.definitions[guid] = {
|
|
190
|
+
...existing,
|
|
191
|
+
...(patch.displayName !== undefined && { displayName: patch.displayName }),
|
|
192
|
+
...(patch.description !== undefined && {
|
|
193
|
+
description: clearable(patch.description) as string | undefined
|
|
194
|
+
}),
|
|
195
|
+
...(patch.category !== undefined && {
|
|
196
|
+
category: clearable(patch.category) as string | undefined
|
|
197
|
+
}),
|
|
198
|
+
...(patch.tags !== undefined && { tags: clearable(patch.tags) as string[] | undefined }),
|
|
199
|
+
...(patch.coverImage !== undefined && {
|
|
200
|
+
coverImage: clearable(patch.coverImage) as string | undefined
|
|
201
|
+
}),
|
|
202
|
+
...(patch.projectId !== undefined && { projectId: patch.projectId }),
|
|
203
|
+
...(patch.computeServerId !== undefined && {
|
|
204
|
+
computeServerId: clearable(patch.computeServerId) as string | undefined
|
|
205
|
+
}),
|
|
206
|
+
...(patch.status !== undefined && { status: patch.status }),
|
|
207
|
+
...(patch.ownerId !== undefined && { ownerId: patch.ownerId }),
|
|
208
|
+
...auditUpdate(ctx, existing.updatedBy ?? existing.ownerId)
|
|
209
|
+
};
|
|
210
|
+
await this.writeConfig(config);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async delete(ctx: RequestContext, guid: string): Promise<void> {
|
|
214
|
+
const config = await this.readConfig();
|
|
215
|
+
const existing = config.definitions[guid];
|
|
216
|
+
if (!this.live(existing)) return;
|
|
217
|
+
Object.assign(existing, auditSoftDelete(ctx, existing.updatedBy ?? existing.ownerId));
|
|
218
|
+
await this.writeConfig(config);
|
|
219
|
+
await this.events.emit({
|
|
220
|
+
type: 'definition.deleted',
|
|
221
|
+
definitionId: guid,
|
|
222
|
+
actorId: actorFrom(ctx)
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async incrementSolveCount(_ctx: RequestContext, guid: string): Promise<void> {
|
|
227
|
+
const config = await this.readConfig();
|
|
228
|
+
const existing = config.definitions[guid];
|
|
229
|
+
if (!this.live(existing)) return;
|
|
230
|
+
existing.solveCount = (existing.solveCount ?? 0) + 1;
|
|
231
|
+
existing.updatedAt = new Date().toISOString();
|
|
232
|
+
await this.writeConfig(config);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// Versions (spec §6)
|
|
237
|
+
// ============================================================================
|
|
238
|
+
|
|
239
|
+
async createVersion(ctx: RequestContext, version: DefinitionVersion): Promise<void> {
|
|
240
|
+
const config = await this.readConfig();
|
|
241
|
+
const parent = config.definitions[version.definitionId];
|
|
242
|
+
if (!this.live(parent)) {
|
|
243
|
+
throw new ProviderError(`Definition '${version.definitionId}' not found`, 404);
|
|
244
|
+
}
|
|
245
|
+
if (config.definitionVersions[version.id]) {
|
|
246
|
+
throw new ProviderError(`Version '${version.id}' already exists`, 409);
|
|
247
|
+
}
|
|
248
|
+
config.definitionVersions[version.id] = { ...version };
|
|
249
|
+
await this.writeConfig(config);
|
|
250
|
+
await this.events.emit({
|
|
251
|
+
type: 'definition_version.created',
|
|
252
|
+
versionId: version.id,
|
|
253
|
+
definitionId: version.definitionId,
|
|
254
|
+
actorId: actorFrom(ctx)
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async listVersions(
|
|
259
|
+
_ctx: RequestContext,
|
|
260
|
+
definitionId: string,
|
|
261
|
+
opts?: ListOptions
|
|
262
|
+
): Promise<Page<DefinitionVersion>> {
|
|
263
|
+
const config = await this.readConfig();
|
|
264
|
+
const parent = config.definitions[definitionId];
|
|
265
|
+
if (!this.live(parent)) return paginate([], opts);
|
|
266
|
+
const rows = Object.values(config.definitionVersions)
|
|
267
|
+
.filter((v) => v.definitionId === definitionId)
|
|
268
|
+
.sort((a, b) => b.versionNumber - a.versionNumber);
|
|
269
|
+
return paginate(rows, opts);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async getVersion(_ctx: RequestContext, versionId: string): Promise<DefinitionVersion | null> {
|
|
273
|
+
const config = await this.readConfig();
|
|
274
|
+
return config.definitionVersions[versionId] ?? null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async deleteVersion(ctx: RequestContext, versionId: string): Promise<void> {
|
|
278
|
+
const config = await this.readConfig();
|
|
279
|
+
const version = config.definitionVersions[versionId];
|
|
280
|
+
if (!version) return;
|
|
281
|
+
const parent = config.definitions[version.definitionId];
|
|
282
|
+
// §6 deletion protection — cannot delete a version while it's serving
|
|
283
|
+
// either channel. Caller must repoint live/draft first.
|
|
284
|
+
if (parent && (parent.liveVersionId === versionId || parent.draftVersionId === versionId)) {
|
|
285
|
+
throw new ProviderError(
|
|
286
|
+
`Version '${versionId}' is referenced by liveVersionId or draftVersionId`,
|
|
287
|
+
409
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
delete config.definitionVersions[versionId];
|
|
291
|
+
await this.writeConfig(config);
|
|
292
|
+
await this.events.emit({
|
|
293
|
+
type: 'definition_version.deleted',
|
|
294
|
+
versionId,
|
|
295
|
+
actorId: actorFrom(ctx)
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async setLiveVersion(
|
|
300
|
+
ctx: RequestContext,
|
|
301
|
+
definitionId: string,
|
|
302
|
+
versionId: string
|
|
303
|
+
): Promise<void> {
|
|
304
|
+
await this.repoint('live', ctx, definitionId, versionId);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async setDraftVersion(
|
|
308
|
+
ctx: RequestContext,
|
|
309
|
+
definitionId: string,
|
|
310
|
+
versionId: string
|
|
311
|
+
): Promise<void> {
|
|
312
|
+
await this.repoint('draft', ctx, definitionId, versionId);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async attachInitialVersion(
|
|
316
|
+
ctx: RequestContext,
|
|
317
|
+
definitionId: string,
|
|
318
|
+
versionId: string
|
|
319
|
+
): Promise<void> {
|
|
320
|
+
// Single read-modify-write pass: both pointers + status updated together.
|
|
321
|
+
// `writeConfig` is one fs operation, so the 'pending' → 'draft'
|
|
322
|
+
// transition is atomic from any subsequent reader's perspective.
|
|
323
|
+
const config = await this.readConfig();
|
|
324
|
+
const record = config.definitions[definitionId];
|
|
325
|
+
if (!this.live(record)) throw new ProviderError(`Definition '${definitionId}' not found`, 404);
|
|
326
|
+
const version = config.definitionVersions[versionId];
|
|
327
|
+
if (!version || version.definitionId !== definitionId) {
|
|
328
|
+
throw new ProviderError(`Version '${versionId}' not found for this definition`, 404);
|
|
329
|
+
}
|
|
330
|
+
record.liveVersionId = versionId;
|
|
331
|
+
record.draftVersionId = versionId;
|
|
332
|
+
record.status = 'draft';
|
|
333
|
+
Object.assign(record, auditUpdate(ctx, record.updatedBy ?? record.ownerId));
|
|
334
|
+
await this.writeConfig(config);
|
|
335
|
+
// No `definition.published` event — see interface doc. The parent's
|
|
336
|
+
// `definition.created` + `definition_version.created` (emitted earlier
|
|
337
|
+
// in this transaction) cover the bootstrap.
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private async repoint(
|
|
341
|
+
channel: 'live' | 'draft',
|
|
342
|
+
ctx: RequestContext,
|
|
343
|
+
definitionId: string,
|
|
344
|
+
versionId: string
|
|
345
|
+
): Promise<void> {
|
|
346
|
+
const config = await this.readConfig();
|
|
347
|
+
const record = config.definitions[definitionId];
|
|
348
|
+
if (!this.live(record)) throw new ProviderError(`Definition '${definitionId}' not found`, 404);
|
|
349
|
+
const version = config.definitionVersions[versionId];
|
|
350
|
+
if (!version || version.definitionId !== definitionId) {
|
|
351
|
+
throw new ProviderError(`Version '${versionId}' not found for this definition`, 404);
|
|
352
|
+
}
|
|
353
|
+
if (channel === 'live') record.liveVersionId = versionId;
|
|
354
|
+
else record.draftVersionId = versionId;
|
|
355
|
+
Object.assign(record, auditUpdate(ctx, record.updatedBy ?? record.ownerId));
|
|
356
|
+
await this.writeConfig(config);
|
|
357
|
+
// Only `live` advancement is the published-event trigger. Draft
|
|
358
|
+
// repointing is silent — it's the editor's working pointer, not a
|
|
359
|
+
// publication signal.
|
|
360
|
+
if (channel === 'live') {
|
|
361
|
+
await this.events.emit({
|
|
362
|
+
type: 'definition.published',
|
|
363
|
+
definitionId,
|
|
364
|
+
versionId,
|
|
365
|
+
actorId: actorFrom(ctx)
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import type {
|
|
3
|
+
IInviteStore,
|
|
4
|
+
IEventSink,
|
|
5
|
+
Invite,
|
|
6
|
+
RequestContext,
|
|
7
|
+
ListOptions,
|
|
8
|
+
Page
|
|
9
|
+
} from '@selvajs/platform';
|
|
10
|
+
import { NoopEventSink, actorFrom } from '@selvajs/platform';
|
|
11
|
+
import { paginate, applyOrder } from './pagination.js';
|
|
12
|
+
import { readJsonFile, writeJsonFile } from './fsJson.js';
|
|
13
|
+
|
|
14
|
+
interface InvitesFile {
|
|
15
|
+
invites: Invite[];
|
|
16
|
+
}
|
|
17
|
+
const EMPTY: InvitesFile = { invites: [] };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Filesystem-backed invite store. No per-call scoping by ctx.userId — the
|
|
21
|
+
* route layer gates admin actions. `getByTokenHash` is the sole unauthenticated
|
|
22
|
+
* read and is scoped by the hashed token (caller hashes the raw URL token
|
|
23
|
+
* before lookup); it hides expired and already-accepted invites so a reused
|
|
24
|
+
* link surfaces a clean error.
|
|
25
|
+
*/
|
|
26
|
+
export class LocalInviteStore implements IInviteStore {
|
|
27
|
+
private readonly filePath: string;
|
|
28
|
+
|
|
29
|
+
static fromEnv(
|
|
30
|
+
env: Record<string, string | undefined>,
|
|
31
|
+
events: IEventSink = new NoopEventSink()
|
|
32
|
+
): LocalInviteStore {
|
|
33
|
+
if (!env.DATA_PATH) throw new Error('Missing required env var: DATA_PATH');
|
|
34
|
+
return new LocalInviteStore(env.DATA_PATH, events);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
dataPath: string,
|
|
39
|
+
private readonly events: IEventSink = new NoopEventSink()
|
|
40
|
+
) {
|
|
41
|
+
this.filePath = path.join(dataPath, 'invites.json');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async load(): Promise<InvitesFile> {
|
|
45
|
+
return readJsonFile<InvitesFile>(this.filePath, EMPTY);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async save(file: InvitesFile): Promise<void> {
|
|
49
|
+
await writeJsonFile(this.filePath, file);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async create(ctx: RequestContext, invite: Invite): Promise<void> {
|
|
53
|
+
const file = await this.load();
|
|
54
|
+
file.invites.push(invite);
|
|
55
|
+
await this.save(file);
|
|
56
|
+
await this.events.emit({
|
|
57
|
+
type: 'invite.created',
|
|
58
|
+
inviteId: invite.id,
|
|
59
|
+
orgId: invite.orgId,
|
|
60
|
+
email: invite.email,
|
|
61
|
+
actorId: actorFrom(ctx)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getByTokenHash(_ctx: RequestContext, tokenHash: string): Promise<Invite | null> {
|
|
66
|
+
const { invites } = await this.load();
|
|
67
|
+
const invite = invites.find((i) => i.tokenHash === tokenHash);
|
|
68
|
+
if (!invite) return null;
|
|
69
|
+
if (invite.acceptedAt) return null;
|
|
70
|
+
if (Date.parse(invite.expiresAt) <= Date.now()) return null;
|
|
71
|
+
return invite;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async listByOrg(_ctx: RequestContext, orgId: string, opts?: ListOptions): Promise<Page<Invite>> {
|
|
75
|
+
const { invites } = await this.load();
|
|
76
|
+
const filtered = invites.filter((i) => i.orgId === orgId);
|
|
77
|
+
return paginate(applyOrder(filtered, opts), opts);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async markAccepted(ctx: RequestContext, id: string, userId: string): Promise<void> {
|
|
81
|
+
const file = await this.load();
|
|
82
|
+
const invite = file.invites.find((i) => i.id === id);
|
|
83
|
+
if (!invite || invite.acceptedAt) return;
|
|
84
|
+
invite.acceptedAt = new Date().toISOString();
|
|
85
|
+
invite.acceptedByUserId = userId;
|
|
86
|
+
await this.save(file);
|
|
87
|
+
await this.events.emit({
|
|
88
|
+
type: 'invite.accepted',
|
|
89
|
+
inviteId: invite.id,
|
|
90
|
+
orgId: invite.orgId,
|
|
91
|
+
userId,
|
|
92
|
+
actorId: actorFrom(ctx)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async revoke(ctx: RequestContext, id: string): Promise<void> {
|
|
97
|
+
const file = await this.load();
|
|
98
|
+
const target = file.invites.find((i) => i.id === id && !i.acceptedAt);
|
|
99
|
+
if (!target) return;
|
|
100
|
+
file.invites = file.invites.filter((i) => i.id !== id || i.acceptedAt);
|
|
101
|
+
await this.save(file);
|
|
102
|
+
await this.events.emit({
|
|
103
|
+
type: 'invite.revoked',
|
|
104
|
+
inviteId: id,
|
|
105
|
+
orgId: target.orgId,
|
|
106
|
+
actorId: actorFrom(ctx)
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async deleteByOrg(_ctx: RequestContext, orgId: string): Promise<void> {
|
|
111
|
+
const file = await this.load();
|
|
112
|
+
const before = file.invites.length;
|
|
113
|
+
file.invites = file.invites.filter((i) => i.orgId !== orgId);
|
|
114
|
+
if (file.invites.length === before) return;
|
|
115
|
+
await this.save(file);
|
|
116
|
+
}
|
|
117
|
+
}
|