refactor(infra): directory structure (#4615)

This commit is contained in:
Joooye_34
2023-10-18 23:30:08 +08:00
committed by GitHub
parent 814d552be8
commit bed9310519
1150 changed files with 539 additions and 584 deletions
@@ -0,0 +1,27 @@
import { describe, expect, test } from 'vitest';
import { isValidIPAddress } from '../is-valid-ip-address.js';
describe('isValidIpAddress', () => {
test('should return true for valid IP address', () => {
['115.42.150.37', '192.168.0.1', '110.234.52.124', 'localhost'].forEach(
ip => {
expect(isValidIPAddress(ip)).toBe(true);
}
);
});
test('should return false for invalid IP address', () => {
[
'210.110',
'255',
'y.y.y.y',
'255.0.0.y',
'666.10.10.20',
'4444.11.11.11',
'33.3333.33.3',
].forEach(ip => {
expect(isValidIPAddress(ip)).toBe(false);
});
});
});
+12
View File
@@ -0,0 +1,12 @@
import type { z } from 'zod';
export type Action<
InputSchema extends z.ZodObject<any, any, any, any>,
Args extends readonly any[],
> = {
id: string;
name: string;
description: string;
inputSchema: InputSchema;
action: (input: z.input<InputSchema>, ...args: Args) => void;
};
+131
View File
@@ -0,0 +1,131 @@
// This file should has not side effect
import type { Workspace } from '@blocksuite/store';
declare global {
interface Window {
appInfo: {
electron: boolean;
schema: string;
};
}
}
//#region runtime variables
export const isBrowser = typeof window !== 'undefined';
export const isServer = !isBrowser && typeof navigator === 'undefined';
export const isDesktop = isBrowser && !!window.appInfo?.electron;
//#endregion
export const DEFAULT_WORKSPACE_NAME = 'Demo Workspace';
export const UNTITLED_WORKSPACE_NAME = 'Untitled';
export const DEFAULT_SORT_KEY = 'updatedDate';
export const MessageCode = {
loginError: 0,
noPermission: 1,
loadListFailed: 2,
getDetailFailed: 3,
createWorkspaceFailed: 4,
getMembersFailed: 5,
updateWorkspaceFailed: 6,
deleteWorkspaceFailed: 7,
inviteMemberFailed: 8,
removeMemberFailed: 9,
acceptInvitingFailed: 10,
getBlobFailed: 11,
leaveWorkspaceFailed: 12,
downloadWorkspaceFailed: 13,
refreshTokenError: 14,
blobTooLarge: 15,
} as const;
export const Messages = {
[MessageCode.loginError]: {
message: 'Login failed',
},
[MessageCode.noPermission]: {
message: 'No permission',
},
[MessageCode.loadListFailed]: {
message: 'Load list failed',
},
[MessageCode.getDetailFailed]: {
message: 'Get detail failed',
},
[MessageCode.createWorkspaceFailed]: {
message: 'Create workspace failed',
},
[MessageCode.getMembersFailed]: {
message: 'Get members failed',
},
[MessageCode.updateWorkspaceFailed]: {
message: 'Update workspace failed',
},
[MessageCode.deleteWorkspaceFailed]: {
message: 'Delete workspace failed',
},
[MessageCode.inviteMemberFailed]: {
message: 'Invite member failed',
},
[MessageCode.removeMemberFailed]: {
message: 'Remove member failed',
},
[MessageCode.acceptInvitingFailed]: {
message: 'Accept inviting failed',
},
[MessageCode.getBlobFailed]: {
message: 'Get blob failed',
},
[MessageCode.leaveWorkspaceFailed]: {
message: 'Leave workspace failed',
},
[MessageCode.downloadWorkspaceFailed]: {
message: 'Download workspace failed',
},
[MessageCode.refreshTokenError]: {
message: 'Refresh token failed',
},
[MessageCode.blobTooLarge]: {
message: 'Blob too large',
},
} as const satisfies {
[key in (typeof MessageCode)[keyof typeof MessageCode]]: {
message: string;
};
};
export class PageNotFoundError extends TypeError {
readonly workspace: Workspace;
readonly pageId: string;
constructor(workspace: Workspace, pageId: string) {
super();
this.workspace = workspace;
this.pageId = pageId;
}
}
export class WorkspaceNotFoundError extends TypeError {
readonly workspaceId: string;
constructor(workspaceId: string) {
super();
this.workspaceId = workspaceId;
}
}
export class QueryParamError extends TypeError {
readonly targetKey: string;
readonly query: unknown;
constructor(targetKey: string, query: unknown) {
super();
this.targetKey = targetKey;
this.query = query;
}
}
export class Unreachable extends Error {
constructor(message?: string) {
super(message);
}
}
+69
View File
@@ -0,0 +1,69 @@
import type { Workspace } from '@blocksuite/store';
import { z } from 'zod';
export const literalValueSchema: z.ZodType<LiteralValue, z.ZodTypeDef> =
z.union([
z.number(),
z.string(),
z.boolean(),
z.array(z.lazy(() => literalValueSchema)),
z.record(z.lazy(() => literalValueSchema)),
]);
export type LiteralValue =
| number
| string
| boolean
| { [K: string]: LiteralValue }
| Array<LiteralValue>;
export const refSchema: z.ZodType<Ref, z.ZodTypeDef> = z.object({
type: z.literal('ref'),
name: z.never(),
});
export type Ref = {
type: 'ref';
name: keyof VariableMap;
};
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface VariableMap {}
export const literalSchema = z.object({
type: z.literal('literal'),
value: literalValueSchema,
});
export type Literal = z.input<typeof literalSchema>;
export const filterSchema = z.object({
type: z.literal('filter'),
left: refSchema,
funcName: z.string(),
args: z.array(literalSchema),
});
export type Filter = z.input<typeof filterSchema>;
export const collectionSchema = z.object({
id: z.string(),
workspaceId: z.string(),
name: z.string(),
pinned: z.boolean().optional(),
filterList: z.array(filterSchema),
allowList: z.array(z.string()).optional(),
excludeList: z.array(z.string()).optional(),
});
export type Collection = z.input<typeof collectionSchema>;
export const tagSchema = z.object({
id: z.string(),
value: z.string(),
color: z.string(),
parentId: z.string().optional(),
});
export type Tag = z.input<typeof tagSchema>;
export type PropertiesMeta = Workspace['meta']['properties'];
+161
View File
@@ -0,0 +1,161 @@
/// <reference types="@blocksuite/global" />
import { assertEquals } from '@blocksuite/global/utils';
import { z } from 'zod';
import { isDesktop, isServer } from './constant.js';
import { UaHelper } from './ua-helper.js';
export const blockSuiteFeatureFlags = z.object({
enable_set_remote_flag: z.boolean(),
enable_block_hub: z.boolean(),
enable_toggle_block: z.boolean(),
enable_bookmark_operation: z.boolean(),
enable_note_index: z.boolean(),
});
export const runtimeFlagsSchema = z.object({
enablePlugin: z.boolean(),
enableTestProperties: z.boolean(),
enableBroadcastChannelProvider: z.boolean(),
enableDebugPage: z.boolean(),
changelogUrl: z.string(),
// see: tools/workers
imageProxyUrl: z.string(),
enablePreloading: z.boolean(),
enableNewSettingModal: z.boolean(),
enableNewSettingUnstableApi: z.boolean(),
enableSQLiteProvider: z.boolean(),
enableNotificationCenter: z.boolean(),
enableCloud: z.boolean(),
enableCaptcha: z.boolean(),
enableEnhanceShareMode: z.boolean(),
// this is for the electron app
serverUrlPrefix: z.string(),
enableMoveDatabase: z.boolean(),
editorFlags: blockSuiteFeatureFlags,
appVersion: z.string(),
editorVersion: z.string(),
});
export type BlockSuiteFeatureFlags = z.infer<typeof blockSuiteFeatureFlags>;
export type RuntimeConfig = z.infer<typeof runtimeFlagsSchema>;
export const platformSchema = z.enum([
'aix',
'android',
'darwin',
'freebsd',
'haiku',
'linux',
'openbsd',
'sunos',
'win32',
'cygwin',
'netbsd',
]);
export type Platform = z.infer<typeof platformSchema>;
type BrowserBase = {
/**
* @example https://app.affine.pro
* @example http://localhost:3000
*/
origin: string;
isDesktop: boolean;
isBrowser: true;
isServer: false;
isDebug: boolean;
// browser special properties
isLinux: boolean;
isMacOs: boolean;
isIOS: boolean;
isSafari: boolean;
isWindows: boolean;
isFireFox: boolean;
isMobile: boolean;
isChrome: boolean;
};
type NonChromeBrowser = BrowserBase & {
isChrome: false;
};
type ChromeBrowser = BrowserBase & {
isSafari: false;
isFireFox: false;
isChrome: true;
chromeVersion: number;
};
type Browser = NonChromeBrowser | ChromeBrowser;
type Server = {
isDesktop: false;
isBrowser: false;
isServer: true;
isDebug: boolean;
};
interface Desktop extends ChromeBrowser {
isDesktop: true;
isBrowser: true;
isServer: false;
isDebug: boolean;
}
export type Environment = Browser | Server | Desktop;
export function setupGlobal() {
if (globalThis.$AFFINE_SETUP) {
return;
}
runtimeFlagsSchema.parse(runtimeConfig);
let environment: Environment;
const isDebug = process.env.NODE_ENV === 'development';
if (isServer) {
environment = {
isDesktop: false,
isBrowser: false,
isServer: true,
isDebug,
} satisfies Server;
} else {
const uaHelper = new UaHelper(navigator);
environment = {
origin: window.location.origin,
isDesktop,
isBrowser: true,
isServer: false,
isDebug,
isLinux: uaHelper.isLinux,
isMacOs: uaHelper.isMacOs,
isSafari: uaHelper.isSafari,
isWindows: uaHelper.isWindows,
isFireFox: uaHelper.isFireFox,
isMobile: uaHelper.isMobile,
isChrome: uaHelper.isChrome,
isIOS: uaHelper.isIOS,
} as Browser;
// Chrome on iOS is still Safari
if (environment.isChrome && !environment.isIOS) {
assertEquals(environment.isSafari, false);
assertEquals(environment.isFireFox, false);
environment = {
...environment,
isSafari: false,
isFireFox: false,
isChrome: true,
chromeVersion: uaHelper.getChromeVersion(),
} satisfies ChromeBrowser;
}
}
globalThis.environment = environment;
globalThis.$AFFINE_SETUP = true;
}
+8
View File
@@ -0,0 +1,8 @@
export function isValidIPAddress(address: string) {
if (address === 'localhost') {
return true;
}
return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(
address
);
}
+7
View File
@@ -0,0 +1,7 @@
export type PageInfo = {
isEdgeless: boolean;
title: string;
id: string;
};
export type GetPageInfoById = (id: string) => PageInfo | undefined;
+78
View File
@@ -0,0 +1,78 @@
import { assertExists } from '@blocksuite/global/utils';
export class UaHelper {
private uaMap;
public isLinux = false;
public isMacOs = false;
public isSafari = false;
public isWindows = false;
public isFireFox = false;
public isMobile = false;
public isChrome = false;
public isIOS = false;
getChromeVersion = (): number => {
const raw = this.navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
assertExists(raw);
return parseInt(raw[2], 10);
};
constructor(private readonly navigator: Navigator) {
this.uaMap = getUa(navigator);
this.initUaFlags();
}
public checkUseragent(isUseragent: keyof ReturnType<typeof getUa>) {
return Boolean(this.uaMap[isUseragent]);
}
private initUaFlags() {
this.isLinux = this.checkUseragent('linux');
this.isMacOs = this.checkUseragent('mac');
this.isSafari = this.checkUseragent('safari');
this.isWindows = this.checkUseragent('win');
this.isFireFox = this.checkUseragent('firefox');
this.isMobile = this.checkUseragent('mobile');
this.isChrome = this.checkUseragent('chrome');
this.isIOS = this.checkUseragent('ios');
}
}
const getUa = (navigator: Navigator) => {
const ua = navigator.userAgent;
const uas = ua.toLowerCase();
const mobile = /iPhone|iPad|iPod|Android/i.test(ua);
const android =
(mobile && (uas.indexOf('android') > -1 || uas.indexOf('linux') > -1)) ||
uas.indexOf('adr') > -1;
const ios = mobile && !android && /Mac OS/i.test(ua);
const mac = !mobile && /Mac OS/i.test(ua);
const iphone = ios && uas.indexOf('iphone') > -1;
const ipad = ios && !iphone;
const wx = /MicroMessenger/i.test(ua);
const chrome = /CriOS/i.test(ua) || /Chrome/i.test(ua);
const tiktok = mobile && /aweme/i.test(ua);
const weibo = mobile && /Weibo/i.test(ua);
const safari =
ios && !chrome && !wx && !weibo && !tiktok && /Safari|Macintosh/i.test(ua);
const firefox = /Firefox/.test(ua);
const win = /windows|win32|win64|wow32|wow64/.test(uas);
const linux = /linux/.test(uas);
return {
ua,
mobile,
android,
ios,
mac,
wx,
chrome,
iphone,
ipad,
safari,
tiktok,
weibo,
win,
linux,
firefox,
};
};
+208
View File
@@ -0,0 +1,208 @@
import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store';
import type {
ActiveDocProvider,
PassiveDocProvider,
Workspace as BlockSuiteWorkspace,
} from '@blocksuite/store';
import type { PropsWithChildren, ReactNode } from 'react';
import type { DataSourceAdapter } from 'y-provider';
import type { Collection } from './filter.js';
export enum WorkspaceSubPath {
ALL = 'all',
SETTING = 'setting',
TRASH = 'trash',
SHARED = 'shared',
}
export interface AffineDownloadProvider extends PassiveDocProvider {
flavour: 'affine-download';
}
/**
* Download the first binary from local IndexedDB
*/
export interface BroadCastChannelProvider extends PassiveDocProvider {
flavour: 'broadcast-channel';
}
/**
* Long polling provider with local IndexedDB
*/
export interface LocalIndexedDBBackgroundProvider
extends DataSourceAdapter,
PassiveDocProvider {
flavour: 'local-indexeddb-background';
}
export interface LocalIndexedDBDownloadProvider extends ActiveDocProvider {
flavour: 'local-indexeddb';
}
export interface SQLiteProvider extends PassiveDocProvider, DataSourceAdapter {
flavour: 'sqlite';
}
export interface SQLiteDBDownloadProvider extends ActiveDocProvider {
flavour: 'sqlite-download';
}
export interface AffineSocketIOProvider
extends PassiveDocProvider,
DataSourceAdapter {
flavour: 'affine-socket-io';
}
type BaseWorkspace = {
flavour: string;
id: string;
blockSuiteWorkspace: BlockSuiteWorkspace;
};
export interface AffineCloudWorkspace extends BaseWorkspace {
flavour: WorkspaceFlavour.AFFINE_CLOUD;
id: string;
blockSuiteWorkspace: BlockSuiteWorkspace;
}
export interface LocalWorkspace extends BaseWorkspace {
flavour: WorkspaceFlavour.LOCAL;
id: string;
blockSuiteWorkspace: BlockSuiteWorkspace;
}
export interface AffinePublicWorkspace extends BaseWorkspace {
flavour: WorkspaceFlavour.AFFINE_PUBLIC;
id: string;
blockSuiteWorkspace: BlockSuiteWorkspace;
}
export type AffineOfficialWorkspace =
| AffineCloudWorkspace
| LocalWorkspace
| AffinePublicWorkspace;
export enum ReleaseType {
// if workspace is not released yet, we will not show it in the workspace list
UNRELEASED = 'unreleased',
STABLE = 'stable',
}
export enum LoadPriority {
HIGH = 1,
MEDIUM = 2,
LOW = 3,
}
export enum WorkspaceFlavour {
/**
* New AFFiNE Cloud Workspace using Nest.js Server.
*/
AFFINE_CLOUD = 'affine-cloud',
LOCAL = 'local',
AFFINE_PUBLIC = 'affine-public',
}
export const settingPanel = {
General: 'general',
Collaboration: 'collaboration',
Publish: 'publish',
Export: 'export',
Sync: 'sync',
} as const;
export const settingPanelValues = [...Object.values(settingPanel)] as const;
export type SettingPanel = (typeof settingPanel)[keyof typeof settingPanel];
// built-in workspaces
export interface WorkspaceRegistry {
[WorkspaceFlavour.LOCAL]: LocalWorkspace;
[WorkspaceFlavour.AFFINE_PUBLIC]: AffinePublicWorkspace;
[WorkspaceFlavour.AFFINE_CLOUD]: AffineCloudWorkspace;
}
export interface WorkspaceCRUD<Flavour extends keyof WorkspaceRegistry> {
create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<string>;
delete: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<void>;
get: (workspaceId: string) => Promise<WorkspaceRegistry[Flavour] | null>;
// not supported yet
// update: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
list: () => Promise<WorkspaceRegistry[Flavour][]>;
}
type UIBaseProps<_Flavour extends keyof WorkspaceRegistry> = {
currentWorkspaceId: string;
};
export type WorkspaceHeaderProps<Flavour extends keyof WorkspaceRegistry> =
UIBaseProps<Flavour> & {
currentEntry:
| {
subPath: WorkspaceSubPath;
}
| {
pageId: string;
};
};
type NewSettingProps<Flavour extends keyof WorkspaceRegistry> =
UIBaseProps<Flavour> & {
onDeleteLocalWorkspace: () => void;
onDeleteCloudWorkspace: () => void;
onLeaveWorkspace: () => void;
onTransformWorkspace: <
From extends keyof WorkspaceRegistry,
To extends keyof WorkspaceRegistry,
>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
) => void;
};
type PageDetailProps<Flavour extends keyof WorkspaceRegistry> =
UIBaseProps<Flavour> & {
currentPageId: string;
onLoadEditor: (page: Page, editor: EditorContainer) => () => void;
};
type PageListProps<_Flavour extends keyof WorkspaceRegistry> = {
blockSuiteWorkspace: BlockSuiteWorkspace;
onOpenPage: (pageId: string, newTab?: boolean) => void;
collection: Collection;
};
interface FC<P> {
(props: P): ReactNode;
}
export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
Header: FC<WorkspaceHeaderProps<Flavour>>;
PageDetail: FC<PageDetailProps<Flavour>>;
PageList: FC<PageListProps<Flavour>>;
NewSettingsDetail: FC<NewSettingProps<Flavour>>;
Provider: FC<PropsWithChildren>;
LoginCard?: FC<object>;
}
export interface AppEvents {
// event there is no workspace
// usually used to initialize workspace adapter
'app:init': () => string[];
// event if you have access to workspace adapter
'app:access': () => Promise<boolean>;
'service:start': () => void;
'service:stop': () => void;
}
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
releaseType: ReleaseType;
flavour: Flavour;
// The Adapter will be loaded according to the priority
loadPriority: LoadPriority;
Events: Partial<AppEvents>;
// Fetch necessary data for the first render
CRUD: WorkspaceCRUD<Flavour>;
UI: WorkspaceUISchema<Flavour>;
}
+132
View File
@@ -0,0 +1,132 @@
/**
* @deprecated Remove this file after we migrate to the new cloud.
*/
import { z } from 'zod';
export interface User {
id: string;
name: string;
email: string;
avatar_url: string;
create_at: string;
}
export interface GetUserByEmailParams {
email: string;
workspace_id: string;
}
export const usageResponseSchema = z.object({
blob_usage: z.object({
usage: z.number(),
max_usage: z.number(),
}),
});
export type UsageResponse = z.infer<typeof usageResponseSchema>;
export interface GetWorkspaceDetailParams {
id: string;
}
export enum WorkspaceType {
Private = 0,
Normal = 1,
}
export enum PermissionType {
Read = 0,
Write = 1,
Admin = 10,
Owner = 99,
}
export const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
avatar_url: z.string(),
created_at: z.number(),
});
export const workspaceSchema = z.object({
id: z.string(),
type: z.nativeEnum(WorkspaceType),
public: z.boolean(),
permission: z.nativeEnum(PermissionType),
});
export type Workspace = z.infer<typeof workspaceSchema>;
export const workspaceDetailSchema = z.object({
...workspaceSchema.shape,
permission: z.undefined(),
owner: userSchema,
member_count: z.number(),
});
export type WorkspaceDetail = z.infer<typeof workspaceDetailSchema>;
export interface Permission {
id: string;
type: PermissionType;
workspace_id: string;
user_id: string;
user_email: string;
accepted: boolean;
create_at: number;
}
export interface RegisteredUser extends User {
type: 'Registered';
}
export interface UnregisteredUser {
type: 'Unregistered';
email: string;
}
export interface Member extends Permission {
user: RegisteredUser | UnregisteredUser;
}
export interface GetWorkspaceMembersParams {
id: string;
}
export interface CreateWorkspaceParams {
name: string;
}
export interface UpdateWorkspaceParams {
id: string;
public: boolean;
}
export interface DeleteWorkspaceParams {
id: string;
}
export interface InviteMemberParams {
id: string;
email: string;
}
export interface RemoveMemberParams {
permissionId: number;
}
export interface AcceptInvitingParams {
invitingCode: string;
}
export interface LeaveWorkspaceParams {
id: number | string;
}
export const createWorkspaceResponseSchema = z.object({
id: z.string(),
public: z.boolean(),
type: z.nativeEnum(WorkspaceType),
created_at: z.number(),
});