Skip to content

Commit

Permalink
feat(core): add models setup and improve composables
Browse files Browse the repository at this point in the history
closes #2

BREAKING CHANGE: composable are now dedicated objects and
should not be object-spread in your definition anymore. Instead of
doing `{ ...publishable }` you must use `{ publishable }`.
  • Loading branch information
paul-thebaud committed Feb 5, 2024
1 parent 67c3301 commit 82cde5a
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 33 deletions.
4 changes: 1 addition & 3 deletions packages/cli/src/templates/renderComposableForDef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,5 @@ type ComposableForDefTemplateData = {
};

export default function renderComposableForDef({ composable }: ComposableForDefTemplateData) {
return `
...${composable}
`.trim();
return composable;
}
9 changes: 9 additions & 0 deletions packages/core/src/model/checks/isComposable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ModelComposable } from '@foscia/core/model/types';
import { SYMBOL_MODEL_COMPOSABLE } from '@foscia/core/symbols';
import { isFosciaType } from '@foscia/shared';

export default function isComposable(
value: unknown,
): value is ModelComposable<any> {
return isFosciaType(value, SYMBOL_MODEL_COMPOSABLE);
}
20 changes: 17 additions & 3 deletions packages/core/src/model/makeComposable.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import makeDefinition from '@foscia/core/model/makeDefinition';
import { ModelInstance, ModelParsedDefinition } from '@foscia/core/model/types';
import makeModelSetup from '@foscia/core/model/makeModelSetup';
import {
ModelComposable,
ModelFlattenDefinition,
ModelInstance,
ModelParsedDefinition,
ModelRawSetup,
} from '@foscia/core/model/types';
import { SYMBOL_MODEL_COMPOSABLE } from '@foscia/core/symbols';

/**
* Create a composable definition which will be used by a model factory.
*
* @param rawDefinition
* @param rawSetup
*/
export default function makeComposable<D extends {} = {}>(
rawDefinition?: D & ThisType<ModelInstance<ModelParsedDefinition<D>>>,
rawDefinition?: D & ThisType<ModelInstance<ModelFlattenDefinition<D>>>,
rawSetup?: ModelRawSetup<D>,
) {
return makeDefinition(rawDefinition) as ModelParsedDefinition<D>;
return {
$FOSCIA_TYPE: SYMBOL_MODEL_COMPOSABLE,
$definition: makeDefinition(rawDefinition),
$setup: makeModelSetup(rawSetup),
} as ModelComposable<ModelParsedDefinition<D>>;
}
5 changes: 5 additions & 0 deletions packages/core/src/model/makeDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import isComposable from '@foscia/core/model/checks/isComposable';
import isPendingPropDef from '@foscia/core/model/checks/isPendingPropDef';
import isPropDef from '@foscia/core/model/checks/isPropDef';
import { ModelParsedDefinition } from '@foscia/core/model/types';
import { Dictionary, eachDescriptors, makeDescriptorHolder } from '@foscia/shared';

function parseDescriptor(key: string, descriptor: PropertyDescriptor) {
if (descriptor.value) {
if (isComposable(descriptor.value)) {
return descriptor.value;
}

if (isPropDef(descriptor.value)) {
return { ...descriptor.value, key };
}
Expand Down
43 changes: 31 additions & 12 deletions packages/core/src/model/makeModelClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import FosciaError from '@foscia/core/errors/fosciaError';
import runHooksSync from '@foscia/core/hooks/runHooksSync';
import { HooksRegistrar } from '@foscia/core/hooks/types';
import logger from '@foscia/core/logger/logger';
import isComposable from '@foscia/core/model/checks/isComposable';
import isIdDef from '@foscia/core/model/checks/isIdDef';
import isPropDef from '@foscia/core/model/checks/isPropDef';
import forceFill from '@foscia/core/model/forceFill';
import makeDefinition from '@foscia/core/model/makeDefinition';
import makeModelSetup from '@foscia/core/model/makeModelSetup';
import id from '@foscia/core/model/props/builders/id';
import takeSnapshot from '@foscia/core/model/snapshots/takeSnapshot';
import {
Expand All @@ -15,9 +17,10 @@ import {
ModelHooksDefinition,
ModelInstance,
ModelRelation,
ModelSetup,
} from '@foscia/core/model/types';
import { SYMBOL_MODEL_CLASS, SYMBOL_MODEL_INSTANCE } from '@foscia/core/symbols';
import { eachDescriptors, isNil, mergeConfig, value } from '@foscia/shared';
import { eachDescriptors, mergeConfig, value } from '@foscia/shared';

const computeDefault = (instance: ModelInstance, def: ModelAttribute | ModelRelation) => {
if (def.default && typeof def.default === 'object') {
Expand All @@ -34,7 +37,10 @@ const createModelClass = (
config: ModelConfig,
definition: object,
hooks: HooksRegistrar<ModelHooksDefinition> | null,
setup: ModelSetup,
) => {
const modelSetup = makeModelSetup(setup);

const ModelClass = function ModelConstructor(this: ModelInstance) {
Object.defineProperty(this, '$FOSCIA_TYPE', { value: SYMBOL_MODEL_INSTANCE });
Object.defineProperty(this, '$model', { value: this.constructor });
Expand Down Expand Up @@ -94,35 +100,39 @@ const createModelClass = (
forceFill(this, { [def.key]: computeDefault(this, def) });
}
});

modelSetup.init.forEach((callback) => callback(this));
} as unknown as ExtendableModel;

Object.defineProperty(ModelClass, '$FOSCIA_TYPE', { value: SYMBOL_MODEL_CLASS });
Object.defineProperty(ModelClass, '$type', { value: type });
Object.defineProperty(ModelClass, '$config', { value: { ...config } });
Object.defineProperty(ModelClass, '$schema', { value: {} });
Object.defineProperty(ModelClass, '$setup', { value: makeModelSetup(setup) });
Object.defineProperty(ModelClass, '$hooks', {
writable: true,
value: Object.entries(hooks ?? {}).reduce((newHooks, [hook, callbacks]) => ({
...newHooks,
[hook]: [...(callbacks ?? [])],
}), {}),
value: hooks ?? {},
});

ModelClass.configure = (newConfig: ModelConfig, override = true) => createModelClass(
ModelClass.$type,
mergeConfig(ModelClass.$config, newConfig, override),
definition,
ModelClass.$hooks,
{},
modelSetup,
);

ModelClass.extend = (rawDefinition?: object) => createModelClass(
ModelClass.$type,
ModelClass.$config,
{ ...definition, ...(rawDefinition ?? {}) },
ModelClass.$hooks,
);
{},
modelSetup,
) as any;

eachDescriptors(makeDefinition(definition), (key, descriptor) => {
const applyDefinition = (
currentDefinition: object,
) => eachDescriptors(currentDefinition, (key, descriptor) => {
if (key === 'type') {
throw new FosciaError(
'`type` is forbidden as a definition key because it may be used with some implementations.',
Expand All @@ -135,16 +145,25 @@ const createModelClass = (
);
}

if (!isNil(descriptor.value) && isPropDef(descriptor.value)) {
if (isComposable(descriptor.value)) {
applyDefinition(descriptor.value.$definition);

modelSetup.boot.push(...descriptor.value.$setup.boot);
modelSetup.init.push(...descriptor.value.$setup.init);
} else if (isPropDef(descriptor.value)) {
ModelClass.$schema[key] = descriptor.value;
} else {
Object.defineProperty(ModelClass.prototype, key, descriptor);
}
});

applyDefinition(makeDefinition(definition));

modelSetup.boot.forEach((callback) => callback(ModelClass));

return ModelClass;
};

export default function makeModelClass(type: string, config: ModelConfig) {
return createModelClass(type, config, { id: id(), lid: id() }, {});
export default function makeModelClass(type: string, config: ModelConfig, setup: ModelSetup) {
return createModelClass(type, config, { id: id(), lid: id() }, {}, setup);
}
20 changes: 17 additions & 3 deletions packages/core/src/model/makeModelFactory.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,44 @@
import makeModelClass from '@foscia/core/model/makeModelClass';
import makeModelSetup from '@foscia/core/model/makeModelSetup';
import {
ExtendableModel,
ModelConfig,
ModelFlattenDefinition,
ModelInstance,
ModelParsedDefinition,
ModelRawSetup,
} from '@foscia/core/model/types';

export default function makeModelFactory<ND extends {} = {}>(
baseConfig?: ModelConfig,
baseRawDefinition?: ND & ThisType<ModelInstance<ModelParsedDefinition<ND>>>,
// eslint-disable-next-line max-len
baseRawDefinition?: ND & ThisType<ModelInstance<ModelFlattenDefinition<ModelParsedDefinition<ND>>>>,
baseRawSetup?: ModelRawSetup<ModelFlattenDefinition<ModelParsedDefinition<ND>>>,
) {
const baseSetup = makeModelSetup(baseRawSetup);

return <D extends {} = {}>(
rawConfig: string | (ModelConfig & { type: string; }),
rawDefinition?: D & ThisType<ModelInstance<ModelParsedDefinition<ND & D>>>,
// eslint-disable-next-line max-len
rawDefinition?: D & ThisType<ModelInstance<ModelFlattenDefinition<ModelParsedDefinition<ND & D>>>>,
rawSetup?: ModelRawSetup<ModelFlattenDefinition<ModelParsedDefinition<ND & D>>>,
) => {
const setup = makeModelSetup(rawSetup);

const { type, ...config } = typeof rawConfig === 'string'
? { type: rawConfig }
: rawConfig;

return makeModelClass(type, {
...baseConfig,
...config,
}, {
boot: [...baseSetup.boot, ...setup.boot],
init: [...baseSetup.init, ...setup.init],
}).extend({
...baseRawDefinition,
...rawDefinition,
// eslint-disable-next-line max-len
}) as ExtendableModel<ModelParsedDefinition<ND & D>, ModelInstance<ModelParsedDefinition<ND & D>>>;
}) as ExtendableModel<ModelFlattenDefinition<ModelParsedDefinition<ND & D>>, ModelInstance<ModelFlattenDefinition<ModelParsedDefinition<ND & D>>>>;
};
}
9 changes: 9 additions & 0 deletions packages/core/src/model/makeModelSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ModelRawSetup, ModelSetup } from '@foscia/core/model/types';
import { wrap } from '@foscia/shared';

export default function makeModelSetup<D extends {} = {}>(rawSetup?: ModelRawSetup<D>) {
return {
boot: [...wrap(rawSetup?.boot)],
init: [...wrap(rawSetup?.init)],
} as ModelSetup<D>;
}
71 changes: 63 additions & 8 deletions packages/core/src/model/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Hookable, HookCallback, SyncHookCallback } from '@foscia/core/hooks/types';
import {
SYMBOL_MODEL_CLASS,
SYMBOL_MODEL_COMPOSABLE,
SYMBOL_MODEL_INSTANCE,
SYMBOL_MODEL_PROP_ATTRIBUTE,
SYMBOL_MODEL_PROP_ID,
Expand All @@ -11,14 +12,17 @@ import {
} from '@foscia/core/symbols';
import { ObjectTransformer } from '@foscia/core/transformers/types';
import {
Arrayable,
Awaitable,
Constructor,
DescriptorHolder,
Dictionary,
FosciaObject,
OmitNever,
Optional,
Prev,
Transformer,
UnionToIntersection,
} from '@foscia/shared';

/**
Expand All @@ -38,6 +42,42 @@ export type ModelConfig = {
[K: string]: any;
};

/**
* Callback run after the model factory tasks (definition parsing, etc.).
*/
export type ModelBootCallback<D extends {}> = (model: Model<D, ModelInstance<D>>) => void;

/**
* Callback run after the model constructor tasks (properties setup, etc.).
*/
export type ModelInitCallback<D extends {}> = (instance: ModelInstance<D>) => void;

/**
* Raw model setup configuration.
*/
export type ModelRawSetup<D extends {} = any> = {
boot?: Arrayable<ModelBootCallback<D>>;
init?: Arrayable<ModelInitCallback<D>>;
};

/**
* Parsed model setup configuration.
*/
export type ModelSetup<D extends {} = any> = {
boot: ModelBootCallback<D>[];
init: ModelInitCallback<D>[];
};

/**
* Model composable definition which can be included on any models.
*/
export type ModelComposable<D extends {}> =
& {
$definition: D;
$setup: ModelSetup<D>;
}
& FosciaObject<typeof SYMBOL_MODEL_COMPOSABLE>;

/**
* Model instance ID default typing.
*/
Expand Down Expand Up @@ -162,18 +202,31 @@ export type ModelParsedDefinitionProp<K, V> =
V extends RawModelAttribute<any, any> ? V & ModelPropNormalized<K>
: V extends RawModelRelation<any, any> ? V & ModelPropNormalized<K>
: V extends RawModelId<any, any> ? V & ModelPropNormalized<K>
: V extends DescriptorHolder<any> ? V
: DescriptorHolder<V>;
: V extends ModelComposable<any> ? never
: V extends DescriptorHolder<any> ? V
: DescriptorHolder<V>;

/**
* The parsed model definition with non attributes/relations properties'
* descriptors wrapped in holders.
* The parsed model definition with non composables/attributes/relations
* properties' descriptors wrapped in holders.
*/
export type ModelParsedDefinition<D extends {} = {}> = {
[K in keyof D]: D[K] extends PendingModelProp<RawModelProp<any, any>>
? ModelParsedDefinitionProp<K, D[K]['definition']> : ModelParsedDefinitionProp<K, D[K]>;
[K in keyof D]: D[K] extends ModelComposable<any>
? D[K] : D[K] extends PendingModelProp<RawModelProp<any, any>>
? ModelParsedDefinitionProp<K, D[K]['definition']> : ModelParsedDefinitionProp<K, D[K]>;
};

/**
* The flatten and parsed model definition with composables properties
* flattened to the definition root.
*/
export type ModelFlattenDefinition<D extends {}> =
& UnionToIntersection<{} | {
[K in keyof D]: D[K] extends ModelComposable<infer CD>
? ModelFlattenDefinition<CD> : never;
}[keyof D]>
& OmitNever<{ [K in keyof D]: D[K] extends ModelComposable<any> ? never : D[K] }>;

/**
* Extract model's IDs, attributes and relations from the whole definition.
*/
Expand Down Expand Up @@ -274,8 +327,10 @@ export type ExtendableModel<D extends {} = any, I extends ModelInstance<D> = any
& {
configure(config?: ModelConfig, override?: boolean): ExtendableModel<D, ModelInstance<D>>;
extend<ND extends {} = {}>(
rawDefinition?: ND & ThisType<ModelInstance<D & ModelParsedDefinition<ND>>>,
): ExtendableModel<D & ModelParsedDefinition<ND>, ModelInstance<D & ModelParsedDefinition<ND>>>;
// eslint-disable-next-line max-len
rawDefinition?: ND & ThisType<ModelInstance<ModelFlattenDefinition<D & ModelParsedDefinition<ND>>>>,
// eslint-disable-next-line max-len
): ExtendableModel<ModelFlattenDefinition<D & ModelParsedDefinition<ND>>, ModelInstance<ModelFlattenDefinition<D & ModelParsedDefinition<ND>>>>;
}
& Model<D, I>;

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export const SYMBOL_MODEL_RELATION_HAS_ONE: unique symbol = Symbol('foscia: mode
export const SYMBOL_MODEL_RELATION_HAS_MANY: unique symbol = Symbol('foscia: model relation has many');
export const SYMBOL_MODEL_CLASS: unique symbol = Symbol('foscia: model class');
export const SYMBOL_MODEL_INSTANCE: unique symbol = Symbol('foscia: model instance');
export const SYMBOL_MODEL_COMPOSABLE: unique symbol = Symbol('foscia: model composable');
2 changes: 1 addition & 1 deletion packages/core/tests/mocks/models/post.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { attr, makeModel, toDateTime } from '@foscia/core';
import commentable from '../composables/commentable';

export default class PostMock extends makeModel('posts', {
...commentable,
commentable,
title: attr<string>(),
body: attr<string | null>(),
publishedAt: attr(toDateTime()).nullable().readOnly(),
Expand Down
6 changes: 6 additions & 0 deletions packages/rest/tests/mocks/composables/commentable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { hasMany, makeComposable } from '@foscia/core';
import CommentMock from '../models/comment.mock';

export default makeComposable({
comments: hasMany(() => CommentMock),
});
6 changes: 3 additions & 3 deletions packages/rest/tests/mocks/models/post.mock.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { attr, hasMany, makeModel, toDateTime } from '@foscia/core';
import CommentMock from './comment.mock';
import { attr, makeModel, toDateTime } from '@foscia/core';
import commentable from '../composables/commentable';

export default class PostMock extends makeModel('posts', {
commentable,
title: attr<string>(),
body: attr<string | null>(),
comments: hasMany(() => CommentMock),
publishedAt: attr(toDateTime()).nullable().readOnly(),
}) {
}
Loading

0 comments on commit 82cde5a

Please sign in to comment.