mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 20:46:38 +08:00
refactor(editor): reorg code structure of store package (#9525)
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { type Disposable, Slot } from '@blocksuite/global/utils';
|
||||
import { computed, type Signal, signal } from '@preact/signals-core';
|
||||
|
||||
import type { Text } from '../../reactive/index.js';
|
||||
import type { Blocks } from '../blocks/blocks.js';
|
||||
import type { YBlock } from './types.js';
|
||||
import type { RoleType } from './zod.js';
|
||||
|
||||
type SignaledProps<Props> = Props & {
|
||||
[P in keyof Props & string as `${P}$`]: Signal<Props[P]>;
|
||||
};
|
||||
/**
|
||||
* The MagicProps function is used to append the props to the class.
|
||||
* For example:
|
||||
*
|
||||
* ```ts
|
||||
* class MyBlock extends MagicProps()<{ foo: string }> {}
|
||||
* const myBlock = new MyBlock();
|
||||
* // You'll get type checking for the foo prop
|
||||
* myBlock.foo = 'bar';
|
||||
* ```
|
||||
*/
|
||||
function MagicProps(): {
|
||||
new <Props>(): Props;
|
||||
} {
|
||||
return class {} as never;
|
||||
}
|
||||
|
||||
const modelLabel = Symbol('model_label');
|
||||
|
||||
// @ts-expect-error allow magic props
|
||||
export class BlockModel<
|
||||
Props extends object = object,
|
||||
PropsSignal extends object = SignaledProps<Props>,
|
||||
> extends MagicProps()<PropsSignal> {
|
||||
private readonly _children = signal<string[]>([]);
|
||||
|
||||
/**
|
||||
* @deprecated use doc instead
|
||||
*/
|
||||
page!: Blocks;
|
||||
|
||||
private readonly _childModels = computed(() => {
|
||||
const value: BlockModel[] = [];
|
||||
this._children.value.forEach(id => {
|
||||
const block = this.page.getBlock$(id);
|
||||
if (block) {
|
||||
value.push(block.model);
|
||||
}
|
||||
});
|
||||
return value;
|
||||
});
|
||||
|
||||
private readonly _onCreated: Disposable;
|
||||
|
||||
private readonly _onDeleted: Disposable;
|
||||
|
||||
childMap = computed(() =>
|
||||
this._children.value.reduce((map, id, index) => {
|
||||
map.set(id, index);
|
||||
return map;
|
||||
}, new Map<string, number>())
|
||||
);
|
||||
|
||||
created = new Slot();
|
||||
|
||||
deleted = new Slot();
|
||||
|
||||
flavour!: string;
|
||||
|
||||
id!: string;
|
||||
|
||||
isEmpty = computed(() => {
|
||||
return this._children.value.length === 0;
|
||||
});
|
||||
|
||||
keys!: string[];
|
||||
|
||||
// This is used to avoid https://stackoverflow.com/questions/55886792/infer-typescript-generic-class-type
|
||||
[modelLabel]: Props = 'type_info_label' as never;
|
||||
|
||||
pop!: (prop: keyof Props & string) => void;
|
||||
|
||||
propsUpdated = new Slot<{ key: string }>();
|
||||
|
||||
role!: RoleType;
|
||||
|
||||
stash!: (prop: keyof Props & string) => void;
|
||||
|
||||
// text is optional
|
||||
text?: Text;
|
||||
|
||||
version!: number;
|
||||
|
||||
yBlock!: YBlock;
|
||||
|
||||
get children() {
|
||||
return this._childModels.value;
|
||||
}
|
||||
|
||||
get doc() {
|
||||
return this.page;
|
||||
}
|
||||
|
||||
set doc(doc: Blocks) {
|
||||
this.page = doc;
|
||||
}
|
||||
|
||||
get parent() {
|
||||
return this.doc.getParent(this);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._onCreated = this.created.once(() => {
|
||||
this._children.value = this.yBlock.get('sys:children').toArray();
|
||||
this.yBlock.get('sys:children').observe(event => {
|
||||
this._children.value = event.target.toArray();
|
||||
});
|
||||
this.yBlock.observe(event => {
|
||||
if (event.keysChanged.has('sys:children')) {
|
||||
this._children.value = this.yBlock.get('sys:children').toArray();
|
||||
}
|
||||
});
|
||||
});
|
||||
this._onDeleted = this.deleted.once(() => {
|
||||
this._onCreated.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.created.dispose();
|
||||
this.deleted.dispose();
|
||||
this.propsUpdated.dispose();
|
||||
}
|
||||
|
||||
firstChild(): BlockModel | null {
|
||||
return this.children[0] || null;
|
||||
}
|
||||
|
||||
lastChild(): BlockModel | null {
|
||||
if (!this.children.length) {
|
||||
return this;
|
||||
}
|
||||
return this.children[this.children.length - 1].lastChild();
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this._onCreated.dispose();
|
||||
this._onDeleted.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Schema } from '../../schema/index.js';
|
||||
import type { Blocks } from '../blocks/blocks.js';
|
||||
import { SyncController } from './sync-controller.js';
|
||||
import type { BlockOptions, YBlock } from './types.js';
|
||||
|
||||
export type BlockViewType = 'bypass' | 'display' | 'hidden';
|
||||
|
||||
export class Block {
|
||||
private readonly _syncController: SyncController;
|
||||
|
||||
blockViewType: BlockViewType = 'display';
|
||||
|
||||
get flavour() {
|
||||
return this._syncController.flavour;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._syncController.id;
|
||||
}
|
||||
|
||||
get model() {
|
||||
return this._syncController.model;
|
||||
}
|
||||
|
||||
get pop() {
|
||||
return this._syncController.pop;
|
||||
}
|
||||
|
||||
get stash() {
|
||||
return this._syncController.stash;
|
||||
}
|
||||
|
||||
get version() {
|
||||
return this._syncController.version;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly schema: Schema,
|
||||
readonly yBlock: YBlock,
|
||||
readonly doc?: Blocks,
|
||||
readonly options: BlockOptions = {}
|
||||
) {
|
||||
const onChange = !options.onChange
|
||||
? undefined
|
||||
: (key: string, value: unknown) => {
|
||||
options.onChange?.(this, key, value);
|
||||
};
|
||||
this._syncController = new SyncController(schema, yBlock, doc, onChange);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { BlockModel } from './block-model.js';
|
||||
|
||||
type PropsInDraft = 'version' | 'flavour' | 'role' | 'id' | 'keys' | 'text';
|
||||
|
||||
type ModelProps<Model> = Model extends BlockModel<infer U> ? U : never;
|
||||
|
||||
export type DraftModel<Model extends BlockModel = BlockModel> = Pick<
|
||||
Model,
|
||||
PropsInDraft
|
||||
> & {
|
||||
children: DraftModel[];
|
||||
} & ModelProps<Model>;
|
||||
|
||||
export function toDraftModel<Model extends BlockModel = BlockModel>(
|
||||
origin: Model
|
||||
): DraftModel<Model> {
|
||||
const { id, version, flavour, role, keys, text, children } = origin;
|
||||
const props = origin.keys.reduce((acc, key) => {
|
||||
return {
|
||||
...acc,
|
||||
[key]: origin[key as keyof Model],
|
||||
};
|
||||
}, {} as ModelProps<Model>);
|
||||
|
||||
return {
|
||||
id,
|
||||
version,
|
||||
flavour,
|
||||
role,
|
||||
keys,
|
||||
text,
|
||||
children: children.map(toDraftModel),
|
||||
...props,
|
||||
} as DraftModel<Model>;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './block.js';
|
||||
export * from './block-model.js';
|
||||
export * from './draft.js';
|
||||
export * from './types.js';
|
||||
export * from './zod.js';
|
||||
@@ -0,0 +1,378 @@
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { effect, signal } from '@preact/signals-core';
|
||||
import { createMutex } from 'lib0/mutex.js';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import {
|
||||
Boxed,
|
||||
createYProxy,
|
||||
native2Y,
|
||||
type UnRecord,
|
||||
y2Native,
|
||||
} from '../../reactive/index.js';
|
||||
import type { Schema } from '../../schema/schema.js';
|
||||
import type { Blocks } from '../blocks/blocks.js';
|
||||
import { BlockModel } from './block-model.js';
|
||||
import type { YBlock } from './types.js';
|
||||
import { internalPrimitives } from './zod.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* SyncController is responsible for syncing the block data with Yjs.
|
||||
* It creates a proxy model that syncs with Yjs and provides a reactive interface.
|
||||
* It also handles the stashing and popping of props.
|
||||
* It will also provide signals for block props.
|
||||
*
|
||||
*/
|
||||
export class SyncController {
|
||||
private _byPassProxy: boolean = false;
|
||||
|
||||
private readonly _byPassUpdate = (fn: () => void) => {
|
||||
this._byPassProxy = true;
|
||||
fn();
|
||||
this._byPassProxy = false;
|
||||
};
|
||||
|
||||
private readonly _mutex = createMutex();
|
||||
|
||||
private readonly _observeYBlockChanges = () => {
|
||||
this.yBlock.observe(event => {
|
||||
event.keysChanged.forEach(key => {
|
||||
const type = event.changes.keys.get(key);
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
if (type.action === 'update' || type.action === 'add') {
|
||||
const value = this.yBlock.get(key);
|
||||
const keyName = key.replace('prop:', '');
|
||||
const proxy = this._getPropsProxy(keyName, value);
|
||||
this._byPassUpdate(() => {
|
||||
// @ts-expect-error allow magic props
|
||||
this.model[keyName] = proxy;
|
||||
const signalKey = `${keyName}$`;
|
||||
this._mutex(() => {
|
||||
if (signalKey in this.model) {
|
||||
// @ts-expect-error allow magic props
|
||||
this.model[signalKey].value = y2Native(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
this.onChange?.(keyName, value);
|
||||
return;
|
||||
}
|
||||
if (type.action === 'delete') {
|
||||
const keyName = key.replace('prop:', '');
|
||||
this._byPassUpdate(() => {
|
||||
// @ts-expect-error allow magic props
|
||||
delete this.model[keyName];
|
||||
if (`${keyName}$` in this.model) {
|
||||
// @ts-expect-error allow magic props
|
||||
this.model[`${keyName}$`].value = undefined;
|
||||
}
|
||||
});
|
||||
this.onChange?.(keyName, undefined);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _stashed = new Set<string | number>();
|
||||
|
||||
readonly flavour: string;
|
||||
|
||||
readonly id: string;
|
||||
|
||||
readonly model: BlockModel;
|
||||
|
||||
readonly pop = (prop: string) => {
|
||||
if (!this._stashed.has(prop)) return;
|
||||
this._popProp(prop);
|
||||
};
|
||||
|
||||
readonly stash = (prop: string) => {
|
||||
if (this._stashed.has(prop)) return;
|
||||
|
||||
this._stashed.add(prop);
|
||||
this._stashProp(prop);
|
||||
};
|
||||
|
||||
readonly version: number;
|
||||
|
||||
readonly yChildren: Y.Array<string[]>;
|
||||
|
||||
constructor(
|
||||
readonly schema: Schema,
|
||||
readonly yBlock: YBlock,
|
||||
readonly doc?: Blocks,
|
||||
readonly onChange?: (key: string, value: unknown) => void
|
||||
) {
|
||||
const { id, flavour, version, yChildren, props } = this._parseYBlock();
|
||||
|
||||
this.id = id;
|
||||
this.flavour = flavour;
|
||||
this.yChildren = yChildren;
|
||||
this.version = version;
|
||||
|
||||
this.model = this._createModel(props);
|
||||
|
||||
this._observeYBlockChanges();
|
||||
}
|
||||
|
||||
private _createModel(props: UnRecord) {
|
||||
const _mutex = this._mutex;
|
||||
const schema = this.schema.flavourSchemaMap.get(this.flavour);
|
||||
if (!schema) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ModelCRUDError,
|
||||
`schema for flavour: ${this.flavour} not found`
|
||||
);
|
||||
}
|
||||
|
||||
const model = schema.model.toModel?.() ?? new BlockModel<object>();
|
||||
const signalWithProps = Object.entries(props).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const data = signal(value);
|
||||
const dispose = effect(() => {
|
||||
const value = data.value;
|
||||
if (!this.model) return;
|
||||
_mutex(() => {
|
||||
// @ts-expect-error allow magic props
|
||||
this.model[key] = value;
|
||||
});
|
||||
});
|
||||
model.deleted.once(dispose);
|
||||
return {
|
||||
...acc,
|
||||
[`${key}$`]: data,
|
||||
[key]: value,
|
||||
};
|
||||
},
|
||||
{} as Record<string, unknown>
|
||||
);
|
||||
Object.assign(model, signalWithProps);
|
||||
|
||||
model.id = this.id;
|
||||
model.version = this.version;
|
||||
model.keys = Object.keys(props);
|
||||
model.flavour = schema.model.flavour;
|
||||
model.role = schema.model.role;
|
||||
model.yBlock = this.yBlock;
|
||||
model.stash = this.stash;
|
||||
model.pop = this.pop;
|
||||
if (this.doc) {
|
||||
model.doc = this.doc;
|
||||
}
|
||||
|
||||
const proxy = new Proxy(model, {
|
||||
has: (target, p) => {
|
||||
return Reflect.has(target, p);
|
||||
},
|
||||
set: (target, p, value, receiver) => {
|
||||
if (
|
||||
!this._byPassProxy &&
|
||||
typeof p === 'string' &&
|
||||
model.keys.includes(p)
|
||||
) {
|
||||
if (this._stashed.has(p)) {
|
||||
setValue(target, p, value);
|
||||
const result = Reflect.set(target, p, value, receiver);
|
||||
this.onChange?.(p, value);
|
||||
return result;
|
||||
}
|
||||
|
||||
const yValue = native2Y(value);
|
||||
this.yBlock.set(`prop:${p}`, yValue);
|
||||
const proxy = this._getPropsProxy(p, yValue);
|
||||
setValue(target, p, value);
|
||||
return Reflect.set(target, p, proxy, receiver);
|
||||
}
|
||||
|
||||
return Reflect.set(target, p, value, receiver);
|
||||
},
|
||||
get: (target, p, receiver) => {
|
||||
return Reflect.get(target, p, receiver);
|
||||
},
|
||||
deleteProperty: (target, p) => {
|
||||
if (
|
||||
!this._byPassProxy &&
|
||||
typeof p === 'string' &&
|
||||
model.keys.includes(p)
|
||||
) {
|
||||
this.yBlock.delete(`prop:${p}`);
|
||||
setValue(target, p, undefined);
|
||||
}
|
||||
|
||||
return Reflect.deleteProperty(target, p);
|
||||
},
|
||||
});
|
||||
|
||||
function setValue(target: BlockModel, p: string, value: unknown) {
|
||||
_mutex(() => {
|
||||
// @ts-expect-error allow magic props
|
||||
target[`${p}$`].value = value;
|
||||
});
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
|
||||
private _getPropsProxy(name: string, value: unknown) {
|
||||
return createYProxy(value, {
|
||||
onChange: () => {
|
||||
this.onChange?.(name, value);
|
||||
const signalKey = `${name}$`;
|
||||
if (signalKey in this.model) {
|
||||
this._mutex(() => {
|
||||
// @ts-expect-error allow magic props
|
||||
this.model[signalKey].value = this.model[name];
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _parseYBlock() {
|
||||
let id: string | undefined;
|
||||
let flavour: string | undefined;
|
||||
let version: number | undefined;
|
||||
let yChildren: Y.Array<string[]> | undefined;
|
||||
const props: Record<string, unknown> = {};
|
||||
|
||||
this.yBlock.forEach((value, key) => {
|
||||
if (key.startsWith('prop:')) {
|
||||
const keyName = key.replace('prop:', '');
|
||||
props[keyName] = this._getPropsProxy(keyName, value);
|
||||
return;
|
||||
}
|
||||
if (key === 'sys:id' && typeof value === 'string') {
|
||||
id = value;
|
||||
return;
|
||||
}
|
||||
if (key === 'sys:flavour' && typeof value === 'string') {
|
||||
flavour = value;
|
||||
return;
|
||||
}
|
||||
if (key === 'sys:children' && value instanceof Y.Array) {
|
||||
yChildren = value;
|
||||
return;
|
||||
}
|
||||
if (key === 'sys:version' && typeof value === 'number') {
|
||||
version = value;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (!id) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ModelCRUDError,
|
||||
'block id is not found when creating model'
|
||||
);
|
||||
}
|
||||
if (!flavour) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ModelCRUDError,
|
||||
'block flavour is not found when creating model'
|
||||
);
|
||||
}
|
||||
if (!yChildren) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ModelCRUDError,
|
||||
'block children is not found when creating model'
|
||||
);
|
||||
}
|
||||
|
||||
const schema = this.schema.flavourSchemaMap.get(flavour);
|
||||
if (!schema) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ModelCRUDError,
|
||||
`schema for flavour: ${flavour} not found`
|
||||
);
|
||||
}
|
||||
const defaultProps = schema.model.props?.(internalPrimitives);
|
||||
|
||||
if (typeof version !== 'number') {
|
||||
// no version found in data, set to schema version
|
||||
version = schema.version;
|
||||
}
|
||||
|
||||
// Set default props if not exists
|
||||
if (defaultProps) {
|
||||
Object.entries(defaultProps).forEach(([key, value]) => {
|
||||
if (key in props) return;
|
||||
|
||||
const yValue = native2Y(value);
|
||||
this.yBlock.set(`prop:${key}`, yValue);
|
||||
props[key] = this._getPropsProxy(key, yValue);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
flavour,
|
||||
version,
|
||||
props,
|
||||
yChildren,
|
||||
};
|
||||
}
|
||||
|
||||
private _popProp(prop: string) {
|
||||
const model = this.model as BlockModel<Record<string, unknown>>;
|
||||
|
||||
const value = model[prop];
|
||||
this._stashed.delete(prop);
|
||||
model[prop] = value;
|
||||
}
|
||||
|
||||
private _stashProp(prop: string) {
|
||||
(this.model as BlockModel<Record<string, unknown>>)[prop] = y2Native(
|
||||
this.yBlock.get(`prop:${prop}`),
|
||||
{
|
||||
transform: (value, origin) => {
|
||||
if (Boxed.is(origin)) {
|
||||
return value;
|
||||
}
|
||||
if (origin instanceof Y.Map) {
|
||||
return new Proxy(value as UnRecord, {
|
||||
get: (target, p, receiver) => {
|
||||
return Reflect.get(target, p, receiver);
|
||||
},
|
||||
set: (target, p, value, receiver) => {
|
||||
const result = Reflect.set(target, p, value, receiver);
|
||||
this.onChange?.(prop, value);
|
||||
return result;
|
||||
},
|
||||
deleteProperty: (target, p) => {
|
||||
const result = Reflect.deleteProperty(target, p);
|
||||
this.onChange?.(prop, undefined);
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
if (origin instanceof Y.Array) {
|
||||
return new Proxy(value as unknown[], {
|
||||
get: (target, p, receiver) => {
|
||||
return Reflect.get(target, p, receiver);
|
||||
},
|
||||
set: (target, p, value, receiver) => {
|
||||
const index = Number(p);
|
||||
if (Number.isNaN(index)) {
|
||||
return Reflect.set(target, p, value, receiver);
|
||||
}
|
||||
const result = Reflect.set(target, p, value, receiver);
|
||||
this.onChange?.(prop, value);
|
||||
return result;
|
||||
},
|
||||
deleteProperty: (target, p) => {
|
||||
const result = Reflect.deleteProperty(target, p);
|
||||
this.onChange?.(p as string, undefined);
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type * as Y from 'yjs';
|
||||
|
||||
import type { BlockModel } from './block-model.js';
|
||||
import type { Block } from './index.js';
|
||||
|
||||
export type YBlock = Y.Map<unknown> & {
|
||||
get(prop: 'sys:id' | 'sys:flavour'): string;
|
||||
get(prop: 'sys:children'): Y.Array<string>;
|
||||
get<T = unknown>(prop: string): T;
|
||||
};
|
||||
|
||||
export type BlockOptions = {
|
||||
onChange?: (block: Block, key: string, value: unknown) => void;
|
||||
};
|
||||
|
||||
export type BlockSysProps = {
|
||||
id: string;
|
||||
flavour: string;
|
||||
children?: BlockModel[];
|
||||
};
|
||||
export type BlockProps = BlockSysProps & Record<string, unknown>;
|
||||
@@ -0,0 +1,124 @@
|
||||
import type * as Y from 'yjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Boxed, Text } from '../../reactive/index.js';
|
||||
import type { BaseBlockTransformer } from '../../transformer/base.js';
|
||||
import type { BlockModel } from './block-model.js';
|
||||
|
||||
const FlavourSchema = z.string();
|
||||
const ParentSchema = z.array(z.string()).optional();
|
||||
const ContentSchema = z.array(z.string()).optional();
|
||||
const role = ['root', 'hub', 'content'] as const;
|
||||
const RoleSchema = z.enum(role);
|
||||
|
||||
export type RoleType = (typeof role)[number];
|
||||
|
||||
export interface InternalPrimitives {
|
||||
Text: (input?: Y.Text | string) => Text;
|
||||
Boxed: <T>(input: T) => Boxed<T>;
|
||||
}
|
||||
|
||||
export const internalPrimitives: InternalPrimitives = Object.freeze({
|
||||
Text: (input: Y.Text | string = '') => new Text(input),
|
||||
Boxed: <T>(input: T) => new Boxed(input),
|
||||
});
|
||||
|
||||
export const BlockSchema = z.object({
|
||||
version: z.number(),
|
||||
model: z.object({
|
||||
role: RoleSchema,
|
||||
flavour: FlavourSchema,
|
||||
parent: ParentSchema,
|
||||
children: ContentSchema,
|
||||
props: z
|
||||
.function()
|
||||
.args(z.custom<InternalPrimitives>())
|
||||
.returns(z.record(z.any()))
|
||||
.optional(),
|
||||
toModel: z.function().args().returns(z.custom<BlockModel>()).optional(),
|
||||
}),
|
||||
transformer: z
|
||||
.function()
|
||||
.args()
|
||||
.returns(z.custom<BaseBlockTransformer>())
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type BlockSchemaType = z.infer<typeof BlockSchema>;
|
||||
|
||||
export type PropsGetter<Props> = (
|
||||
internalPrimitives: InternalPrimitives
|
||||
) => Props;
|
||||
|
||||
export type SchemaToModel<
|
||||
Schema extends {
|
||||
model: {
|
||||
props: PropsGetter<object>;
|
||||
flavour: string;
|
||||
};
|
||||
},
|
||||
> = BlockModel<ReturnType<Schema['model']['props']>> &
|
||||
ReturnType<Schema['model']['props']> & {
|
||||
flavour: Schema['model']['flavour'];
|
||||
};
|
||||
|
||||
export function defineBlockSchema<
|
||||
Flavour extends string,
|
||||
Role extends RoleType,
|
||||
Props extends object,
|
||||
Metadata extends Readonly<{
|
||||
version: number;
|
||||
role: Role;
|
||||
parent?: string[];
|
||||
children?: string[];
|
||||
}>,
|
||||
Model extends BlockModel<Props>,
|
||||
Transformer extends BaseBlockTransformer<Props>,
|
||||
>(options: {
|
||||
flavour: Flavour;
|
||||
metadata: Metadata;
|
||||
props?: (internalPrimitives: InternalPrimitives) => Props;
|
||||
toModel?: () => Model;
|
||||
transformer?: () => Transformer;
|
||||
}): {
|
||||
version: number;
|
||||
model: {
|
||||
props: PropsGetter<Props>;
|
||||
flavour: Flavour;
|
||||
} & Metadata;
|
||||
transformer?: () => Transformer;
|
||||
};
|
||||
|
||||
export function defineBlockSchema({
|
||||
flavour,
|
||||
props,
|
||||
metadata,
|
||||
toModel,
|
||||
transformer,
|
||||
}: {
|
||||
flavour: string;
|
||||
metadata: {
|
||||
version: number;
|
||||
role: RoleType;
|
||||
parent?: string[];
|
||||
children?: string[];
|
||||
};
|
||||
props?: (internalPrimitives: InternalPrimitives) => Record<string, unknown>;
|
||||
toModel?: () => BlockModel;
|
||||
transformer?: () => BaseBlockTransformer;
|
||||
}): BlockSchemaType {
|
||||
const schema = {
|
||||
version: metadata.version,
|
||||
model: {
|
||||
role: metadata.role,
|
||||
parent: metadata.parent,
|
||||
children: metadata.children,
|
||||
flavour,
|
||||
props,
|
||||
toModel,
|
||||
},
|
||||
transformer,
|
||||
} satisfies z.infer<typeof BlockSchema>;
|
||||
BlockSchema.parse(schema);
|
||||
return schema;
|
||||
}
|
||||
Reference in New Issue
Block a user