feat(editor): schema extension (#10447)

1. **Major Architectural Change: Schema Management**
   - Moved from `workspace.schema` to `store.schema` throughout the codebase
   - Removed schema property from Workspace and Doc interfaces
   - Added `BlockSchemaExtension` pattern across multiple block types

2. **Block Schema Extensions Added**
   - Added new `BlockSchemaExtension` to numerous block types including:
     - DataView, Surface, Attachment, Bookmark, Code
     - Database, Divider, EdgelessText, Embed blocks (Figma, Github, HTML, etc.)
     - Frame, Image, Latex, List, Note, Paragraph
     - Root, Surface Reference, Table blocks

3. **Import/Export System Updates**
   - Updated import functions to accept `schema` parameter:
     - `importHTMLToDoc`
     - `importHTMLZip`
     - `importMarkdownToDoc`
     - `importMarkdownZip`
     - `importNotionZip`
   - Modified export functions to use new schema pattern

4. **Test Infrastructure Updates**
   - Updated test files to use new schema extensions
   - Modified test document creation to include schema extensions
   - Removed direct schema registration in favor of extensions

5. **Service Layer Changes**
   - Updated various services to use `getAFFiNEWorkspaceSchema()`
   - Modified transformer initialization to use document schema
   - Updated collection initialization patterns

6. **Version Management**
   - Removed version-related properties and methods from:
     - `WorkspaceMetaImpl`
     - `TestMeta`
     - `DocImpl`
   - Removed `blockVersions` and `workspaceVersion/pageVersion`

7. **Store and Extension Updates**
   - Added new store extensions and adapters
   - Updated store initialization patterns
   - Added new schema-related functionality in store extension

This PR represents a significant architectural shift in how schemas are managed, moving from a workspace-centric to a store-centric approach, while introducing a more extensible block schema system through `BlockSchemaExtension`. The changes touch multiple layers of the application including core functionality, services, testing infrastructure, and import/export capabilities.
This commit is contained in:
Saul-Mirone
2025-02-26 11:31:28 +00:00
parent 2732b96d00
commit ce87dcf58e
95 changed files with 655 additions and 490 deletions

View File

@@ -16,6 +16,7 @@ import {
initDocFromProps,
} from '../../../blocksuite/initialization';
import type { DocProperties } from '../../db';
import { getAFFiNEWorkspaceSchema } from '../../workspace';
import type { Doc } from '../entities/doc';
import { DocPropertyList } from '../entities/property-list';
import { DocRecordList } from '../entities/record-list';
@@ -202,7 +203,7 @@ export class DocsService extends Service {
const collection = this.store.getBlocksuiteCollection();
const transformer = new Transformer({
schema: collection.schema,
schema: getAFFiNEWorkspaceSchema(),
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),

View File

@@ -116,7 +116,6 @@ const bookmarkFlavours = new Set([
const markdownPreviewDocCollection = new WorkspaceImpl({
id: 'indexer',
schema: blocksuiteSchema,
});
function generateMarkdownPreviewBuilder(
@@ -190,7 +189,7 @@ function generateMarkdownPreviewBuilder(
const provider = container.provider();
const markdownAdapter = new MarkdownAdapter(
new Transformer({
schema: markdownPreviewDocCollection.schema,
schema: getAFFiNEWorkspaceSchema(),
blobCRUD: markdownPreviewDocCollection.blobSync,
docCRUD: {
create: (id: string) => markdownPreviewDocCollection.createDoc({ id }),

View File

@@ -2,7 +2,11 @@ import { type DocMode, ZipTransformer } from '@blocksuite/affine/blocks';
import { Service } from '@toeverything/infra';
import { DocsService } from '../../doc';
import type { WorkspaceMetadata, WorkspacesService } from '../../workspace';
import {
getAFFiNEWorkspaceSchema,
type WorkspaceMetadata,
type WorkspacesService,
} from '../../workspace';
export class ImportTemplateService extends Service {
constructor(private readonly workspacesService: WorkspacesService) {
@@ -21,6 +25,7 @@ export class ImportTemplateService extends Service {
await workspace.engine.doc.waitForDocReady(workspace.id); // wait for root doc ready
const [importedDoc] = await ZipTransformer.importDocs(
workspace.docCollection,
getAFFiNEWorkspaceSchema(),
new Blob([docBinary], {
type: 'application/zip',
})

View File

@@ -53,13 +53,12 @@ import {
WorkspaceServerService,
} from '../../cloud';
import type { GlobalState } from '../../storage';
import {
getAFFiNEWorkspaceSchema,
type Workspace,
type WorkspaceFlavourProvider,
type WorkspaceFlavoursProvider,
type WorkspaceMetadata,
type WorkspaceProfileInfo,
import type {
Workspace,
WorkspaceFlavourProvider,
WorkspaceFlavoursProvider,
WorkspaceMetadata,
WorkspaceProfileInfo,
} from '../../workspace';
import { WorkspaceImpl } from '../../workspace/impls/workspace';
import { getWorkspaceProfileWorker } from './out-worker';
@@ -163,7 +162,6 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
const docCollection = new WorkspaceImpl({
id: workspaceId,
schema: getAFFiNEWorkspaceSchema(),
blobSource: {
get: async key => {
const record = await blobStorage.get(key);

View File

@@ -32,12 +32,11 @@ import { Observable } from 'rxjs';
import { type Doc as YDoc, encodeStateAsUpdate } from 'yjs';
import { DesktopApiService } from '../../desktop-api';
import {
getAFFiNEWorkspaceSchema,
type WorkspaceFlavourProvider,
type WorkspaceFlavoursProvider,
type WorkspaceMetadata,
type WorkspaceProfileInfo,
import type {
WorkspaceFlavourProvider,
WorkspaceFlavoursProvider,
WorkspaceMetadata,
WorkspaceProfileInfo,
} from '../../workspace';
import { WorkspaceImpl } from '../../workspace/impls/workspace';
import { getWorkspaceProfileWorker } from './out-worker';
@@ -145,7 +144,6 @@ class LocalWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
const docCollection = new WorkspaceImpl({
id: id,
schema: getAFFiNEWorkspaceSchema(),
blobSource: {
get: async key => {
const record = await blobStorage.get(key);

View File

@@ -3,7 +3,6 @@ import { Entity, LiveData } from '@toeverything/infra';
import { Observable } from 'rxjs';
import type { Awareness } from 'y-protocols/awareness.js';
import { getAFFiNEWorkspaceSchema } from '../global-schema';
import { WorkspaceImpl } from '../impls/workspace';
import type { WorkspaceScope } from '../scopes/workspace';
import { WorkspaceEngineService } from '../services/engine';
@@ -51,7 +50,6 @@ export class Workspace extends Entity {
name: 'blob',
readonly: false,
},
schema: getAFFiNEWorkspaceSchema(),
onLoadDoc: doc => this.engine.doc.connectDoc(doc),
onLoadAwareness: awareness =>
this.engine.awareness.connectAwareness(awareness),

View File

@@ -147,10 +147,6 @@ export class DocImpl implements Doc {
return this._ready;
}
get schema() {
return this.workspace.schema;
}
get spaceDoc() {
return this._ySpaceDoc;
}
@@ -174,13 +170,6 @@ export class DocImpl implements Doc {
return (readonly?.toString() as 'true' | 'false') ?? 'false';
}
private _handleVersion() {
// Initialization from empty yDoc, indicating that the document is new.
if (!this.workspace.meta.hasVersion) {
this.workspace.meta.writeVersion(this.workspace);
}
}
private _handleYBlockAdd(id: string) {
this.slots.yBlockUpdated.emit({ type: 'add', id });
}
@@ -296,7 +285,6 @@ export class DocImpl implements Doc {
const doc = new Store({
doc: this,
schema: this.workspace.schema,
readonly,
query,
provider,
@@ -316,10 +304,6 @@ export class DocImpl implements Doc {
this.spaceDoc.load();
this.workspace.onLoadDoc?.(this.spaceDoc);
if ((this.workspace.meta.docs?.length ?? 0) <= 1) {
this._handleVersion();
}
this._initYBlocks();
this._yBlocks.forEach((_, id) => {

View File

@@ -3,20 +3,13 @@ import {
createYProxy,
type DocMeta,
type DocsPropertiesMeta,
type Workspace,
type WorkspaceMeta,
} from '@blocksuite/affine/store';
import type * as Y from 'yjs';
const COLLECTION_VERSION = 2;
const PAGE_VERSION = 2;
type MetaState = {
pages?: unknown[];
properties?: DocsPropertiesMeta;
workspaceVersion?: number;
pageVersion?: number;
blockVersions?: Record<string, number>;
name?: string;
avatar?: string;
};
@@ -102,25 +95,6 @@ export class WorkspaceMetaImpl implements WorkspaceMeta {
return this._proxy.pages;
}
get hasVersion() {
if (!this._blockVersions || !this._pageVersion || !this._workspaceVersion) {
return false;
}
return Object.keys(this._blockVersions).length > 0;
}
private get _blockVersions() {
return this._proxy.blockVersions;
}
private get _pageVersion() {
return this._proxy.pageVersion;
}
private get _workspaceVersion() {
return this._proxy.workspaceVersion;
}
get yDocs() {
return this._yMap.get('pages') as unknown as Y.Array<unknown>;
}
@@ -220,33 +194,4 @@ export class WorkspaceMetaImpl implements WorkspaceMeta {
});
}, this._doc.clientID);
}
/**
* @internal Only for doc initialization
*/
writeVersion(collection: Workspace) {
const { blockVersions, pageVersion, workspaceVersion } = this._proxy;
if (!workspaceVersion) {
this._proxy.workspaceVersion = COLLECTION_VERSION;
} else {
console.error('Workspace version is already set');
}
if (!pageVersion) {
this._proxy.pageVersion = PAGE_VERSION;
} else {
console.error('Doc version is already set');
}
if (!blockVersions) {
const _versions: Record<string, number> = {};
collection.schema.flavourSchemaMap.forEach((schema, flavour) => {
_versions[flavour] = schema.version;
});
this._proxy.blockVersions = _versions;
} else {
console.error('Block versions is already set');
}
}
}

View File

@@ -10,7 +10,6 @@ import {
type GetBlocksOptions,
type IdGenerator,
nanoid,
type Schema,
type Store,
type Workspace,
type WorkspaceMeta,
@@ -28,15 +27,12 @@ import { WorkspaceMetaImpl } from './meta';
type WorkspaceOptions = {
id?: string;
schema: Schema;
blobSource?: BlobSource;
onLoadDoc?: (doc: Y.Doc) => void;
onLoadAwareness?: (awareness: Awareness) => void;
};
export class WorkspaceImpl implements Workspace {
protected readonly _schema: Schema;
readonly awarenessStore: AwarenessStore;
readonly blobSync: BlobEngine;
@@ -61,22 +57,15 @@ export class WorkspaceImpl implements Workspace {
return this.blockCollections;
}
get schema() {
return this._schema;
}
readonly onLoadDoc?: (doc: Y.Doc) => void;
readonly onLoadAwareness?: (awareness: Awareness) => void;
constructor({
id,
schema,
blobSource,
onLoadDoc,
onLoadAwareness,
}: WorkspaceOptions) {
this._schema = schema;
}: WorkspaceOptions = {}) {
this.id = id || '';
this.doc = new Y.Doc({ guid: id });
this.awarenessStore = new AwarenessStore(new Awareness(this.doc));