refactor(server): plugin modules (#5630)
- [x] separates modules into `fundamental`, `core`, `plugins`
- [x] optional modules with `@OptionalModule` decorator to install modules with requirements met(`requires`, `if`)
- [x] `module.contributesTo` defines optional features that will be enabled if module registered
- [x] `AFFiNE.plugins.use('payment', {})` to enable a optional/plugin module
- [x] `PaymentModule` is the first plugin module
- [x] GraphQLSchema will not be generated for non-included modules
- [x] Frontend can use `ServerConfigType` query to detect which features are enabled
- [x] override existing provider globally
This commit is contained in:
82
packages/backend/server/src/core/utils/__tests__/doc.spec.ts
Normal file
82
packages/backend/server/src/core/utils/__tests__/doc.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import test from 'ava';
|
||||
|
||||
import { DocID, DocVariant } from '../doc';
|
||||
|
||||
test('can parse', t => {
|
||||
// workspace only
|
||||
let id = new DocID('ws');
|
||||
|
||||
t.is(id.workspace, 'ws');
|
||||
t.assert(id.isWorkspace);
|
||||
|
||||
// full id
|
||||
id = new DocID('ws:space:sub');
|
||||
|
||||
t.is(id.workspace, 'ws');
|
||||
t.is(id.variant, DocVariant.Space);
|
||||
t.is(id.guid, 'sub');
|
||||
|
||||
// variant only
|
||||
id = new DocID('space:sub', 'ws');
|
||||
|
||||
t.is(id.workspace, 'ws');
|
||||
t.is(id.variant, DocVariant.Space);
|
||||
t.is(id.guid, 'sub');
|
||||
|
||||
// sub id only
|
||||
id = new DocID('sub', 'ws');
|
||||
|
||||
t.is(id.workspace, 'ws');
|
||||
t.is(id.variant, DocVariant.Unknown);
|
||||
t.is(id.guid, 'sub');
|
||||
});
|
||||
|
||||
test('fail', t => {
|
||||
t.throws(() => new DocID('a:b:c:d'), {
|
||||
message: 'Invalid format of Doc ID: a:b:c:d',
|
||||
});
|
||||
t.throws(() => new DocID(':space:sub'), { message: 'Workspace is required' });
|
||||
t.throws(() => new DocID('space:sub'), { message: 'Workspace is required' });
|
||||
t.throws(() => new DocID('ws:any:sub'), {
|
||||
message: 'Invalid ID variant: any',
|
||||
});
|
||||
t.throws(() => new DocID('ws:space:'), {
|
||||
message: 'ID is required for non-workspace doc',
|
||||
});
|
||||
t.throws(() => new DocID('ws::space'), {
|
||||
message: 'Variant is required for non-workspace doc',
|
||||
});
|
||||
});
|
||||
|
||||
test('fix', t => {
|
||||
let id = new DocID('ws');
|
||||
// can't fix because the doc variant is [Workspace]
|
||||
id.fixWorkspace('ws2');
|
||||
t.is(id.workspace, 'ws');
|
||||
t.is(id.toString(), 'ws');
|
||||
|
||||
id = new DocID('ws:space:sub');
|
||||
|
||||
id.fixWorkspace('ws2');
|
||||
|
||||
t.is(id.workspace, 'ws2');
|
||||
t.is(id.toString(), 'ws2:space:sub');
|
||||
|
||||
id = new DocID('space:sub', 'ws');
|
||||
t.is(id.workspace, 'ws');
|
||||
t.is(id.toString(), 'ws:space:sub');
|
||||
|
||||
id = new DocID('ws2:space:sub', 'ws');
|
||||
t.is(id.workspace, 'ws');
|
||||
t.is(id.toString(), 'ws:space:sub');
|
||||
});
|
||||
|
||||
test('special case: `wsId:space:page:pageId`', t => {
|
||||
const id = new DocID('ws:space:page:page');
|
||||
t.is(id.workspace, 'ws');
|
||||
t.is(id.guid, 'page');
|
||||
|
||||
t.throws(() => new DocID('ws:s:p:page'));
|
||||
t.throws(() => new DocID('ws:space:b:page'));
|
||||
t.throws(() => new DocID('ws:s:page:page'));
|
||||
});
|
||||
121
packages/backend/server/src/core/utils/doc.ts
Normal file
121
packages/backend/server/src/core/utils/doc.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum DocVariant {
|
||||
Workspace = 'workspace',
|
||||
Page = 'page',
|
||||
Space = 'space',
|
||||
Settings = 'settings',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
registerEnumType(DocVariant, {
|
||||
name: 'DocVariant',
|
||||
});
|
||||
|
||||
export class DocID {
|
||||
raw: string;
|
||||
workspace: string;
|
||||
variant: DocVariant;
|
||||
private readonly sub: string | null;
|
||||
|
||||
static parse(raw: string): DocID | null {
|
||||
try {
|
||||
return new DocID(raw);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* pure guid for workspace and subdoc without any prefix
|
||||
*/
|
||||
get guid(): string {
|
||||
return this.variant === DocVariant.Workspace
|
||||
? this.workspace
|
||||
: // sub is always truthy when variant is not workspace
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.sub!;
|
||||
}
|
||||
|
||||
get full(): string {
|
||||
return this.variant === DocVariant.Workspace
|
||||
? this.workspace
|
||||
: `${this.workspace}:${this.variant}:${this.sub}`;
|
||||
}
|
||||
|
||||
get isWorkspace(): boolean {
|
||||
return this.variant === DocVariant.Workspace;
|
||||
}
|
||||
|
||||
constructor(raw: string, workspaceId?: string) {
|
||||
if (!raw.length) {
|
||||
throw new Error('Invalid Empty Doc ID');
|
||||
}
|
||||
|
||||
let parts = raw.split(':');
|
||||
|
||||
if (parts.length > 3) {
|
||||
// special adapt case `wsId:space:page:pageId`
|
||||
if (parts[1] === DocVariant.Space && parts[2] === DocVariant.Page) {
|
||||
parts = [workspaceId ?? parts[0], DocVariant.Space, parts[3]];
|
||||
} else {
|
||||
throw new Error(`Invalid format of Doc ID: ${raw}`);
|
||||
}
|
||||
} else if (parts.length === 2) {
|
||||
// `${variant}:${guid}`
|
||||
if (!workspaceId) {
|
||||
throw new Error('Workspace is required');
|
||||
}
|
||||
|
||||
parts.unshift(workspaceId);
|
||||
} else if (parts.length === 1) {
|
||||
// ${ws} or ${pageId}
|
||||
if (workspaceId && parts[0] !== workspaceId) {
|
||||
parts = [workspaceId, DocVariant.Unknown, parts[0]];
|
||||
} else {
|
||||
// parts:[ws] equals [workspaceId]
|
||||
}
|
||||
}
|
||||
|
||||
let workspace = parts.at(0);
|
||||
|
||||
// fix for `${non-workspaceId}:${variant}:${guid}`
|
||||
if (workspaceId) {
|
||||
workspace = workspaceId;
|
||||
}
|
||||
|
||||
const variant = parts.at(1);
|
||||
const docId = parts.at(2);
|
||||
|
||||
if (!workspace) {
|
||||
throw new Error('Workspace is required');
|
||||
}
|
||||
|
||||
if (variant) {
|
||||
if (!Object.values(DocVariant).includes(variant as any)) {
|
||||
throw new Error(`Invalid ID variant: ${variant}`);
|
||||
}
|
||||
|
||||
if (!docId) {
|
||||
throw new Error('ID is required for non-workspace doc');
|
||||
}
|
||||
} else if (docId) {
|
||||
throw new Error('Variant is required for non-workspace doc');
|
||||
}
|
||||
|
||||
this.raw = raw;
|
||||
this.workspace = workspace;
|
||||
this.variant = (variant as DocVariant | undefined) ?? DocVariant.Workspace;
|
||||
this.sub = docId || null;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.full;
|
||||
}
|
||||
|
||||
fixWorkspace(workspaceId: string) {
|
||||
if (!this.isWorkspace && this.workspace !== workspaceId) {
|
||||
this.workspace = workspaceId;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user