Closed alexandrebodin closed 2 years ago
This pull request has been mentioned on Strapi Community Forum. There might be relevant details there:
https://forum.strapi.io/t/new-version-questions-thoughts-v4/4824/1
Many plugins I've seen for gatsby extend in some way these definitions, so I'm wondering whether it would make sense the prefix internal properties. E.g.:
const articleDef = {
attributes: {
title: {
$type: 'string',
},
seo: {
$type: 'component',
$component: 'seo',
},
},
};
This would make it easier for you to introduce new properties in the future, without breaking anything. Otherwise you could provide an interface, which plugins then could use for extending the definitions with their own properties. This would also limit the risk of two plugins overwriting the same property ...
Many plugins I've seen for gatsby extend in some way these definitions, so I'm wondering whether it would make sense the prefix internal properties. E.g.:
const articleDef = { attributes: { title: { $type: 'string', }, seo: { $type: 'component', $component: 'seo', }, }, };
This would make it easier for you to introduce new properties in the future, without breaking anything. Otherwise you could provide an interface, which plugins then could use for extending the definitions with their own properties. This would also limit the risk of two plugins overwriting the same property ...
We already have a way to avoid plugins from creating conflicts with a specific property for each plugin to have their own scope. It makes it verbose for sure but avoids using special prefix or characters and keep each property with their original meaning.
const def = {
attributes: {
title: {
pluginOptions : {
pluginName: {
...
}
}
}
}
@alexandrebodin Ah, nice. Didn't know about this. Maybe it would make sense to lint the file for non-core attributes and to output warnings for plugin developers. I think pluginName
needs some definition though: should it use it's full name (e.g. strapi-plugin-algolia
) or a shortcut (e.g. algolia
). I think this could be a convention, but should be mentioned somewhere in the docs :)
Hello,
Even if Strapi v4 will be written in JavaScript, I think it could be a good idea to design this feature with Typescript (or Jsdoc equivalent) :
strapi
or strapi-utils
that will resolve defaults values, so we don't have to manage undefined
in core/pluginsimport { defineModel } from 'strapi'
export default defineModel({
attributes: {
title: {
type: 'string',
},
images: {
type: 'media',
},
},
})
/**
* same as:
*
* export default {
* attributes: {
* title: {
* type: 'string',
* unique: false,
* required: false,
* },
* images: {
* type: 'media',
* multiple: false,
* required: false,
* },
* }
* }
*/
note: this is inspired by vitejs
defineConfig
/ vue3defineComponent
syntax see more here: https://vitejs.dev/config/
this is only sugar syntax, but it enables excellent DX
The idea behind defineModel
is it takes a configuration object defined by user, which allow undefined
values where it is relevant
(see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L67) and resolve it with a frozen object with all default values set (see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L195)
Using typescript interfaces (and not types) to describes attributes allow plugins to extend their definitions:
// in strapi-core
interface MediaAttribute {
type: 'media'
multiple: boolean
}
// in custom plugin "strapi-plugin-media-sharp"
interface MediaAttribute {
format: {
[ name: string ]: {
width: number
height: number
fit: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'
}
}
}
we may have to handle a custom hook in the defineModel
function to allow plugins to extend / check the model
/**
* Internal API
*/
// internal core
interface Strapi {
start: () => Promise<void>;
use: <T>(model: Model<T>) => void;
}
function createStrapi(): Strapi {
return {
use: () => {},
start: async () => {},
};
}
// internal model api
type Model<T> = Partial<{
attributes: T;
lifecycle: ModelLifecycle<T>;
controllers: any;
services: any;
}>;
interface ModelLifecycle<T> {
beforeCreated?: (entity: T) => T | Promise<T>;
afterCreated?: (entity: T) => void | Promise<void>;
}
function defineModel<T>(definition: { (): T }): Model<T> {
return definition();
}
// internal attributes api
type Attributes = {
[identifier: string]:
| MediaAttribute
| StringAttribute
| string
| NumberAttribute
| number
| BooleanAttribute
| boolean
| ComponentAttribute
| DynamicZoneAttribute;
};
function useAttributes<T extends Attributes>(t?: Partial<T>): Partial<T> {
// return type should be Required<Readonly<T>>
return t || {};
}
interface MediaAttribute {
type: "media";
multiple: boolean;
}
interface StringAttribute {
type: "string";
required?: boolean;
}
interface NumberAttribute {
type: "number";
required?: boolean;
min?: number;
max?: number;
}
interface BooleanAttribute {
type: "boolean";
default?: boolean;
}
interface ComponentAttribute {
type: "component";
component: string;
}
interface DynamicZoneAttribute {
type: "dynamiczone";
components: string[];
}
// internal lifecycle api
function useLifecycle<T>(definition: ModelLifecycle<T>): ModelLifecycle<T> {
return definition;
}
// internal meta api
function useMeta<T>(definition: { (): T }): Partial<T> {
// return type should be Required<T>
return definition();
}
function primaryKey(): string {
return "";
}
function createdAt(): Date {
return new Date();
}
function updatedAt(): Date {
return new Date();
}
function relationTo<T>(model: T): any {
return {};
}
/**
* User API
*/
// custom category model
const category = defineModel(() => {
// define model attributes
const attributes =
useAttributes<{
slug: string;
name: string;
}>();
return {
...attributes,
};
});
// custom product model
const product = defineModel(() => {
// define model attributes
const attributes = useAttributes<{
slug: string; // may default to StringAttrubute
name: string;
price: number;
isFeatured: boolean;
picture?: MediaAttribute; // media attribute
description?: string; // optional attributes
}>({
isFeatured: false, // default value
});
// define model meta
const meta = useMeta(() => ({
id: primaryKey(),
createdAt: createdAt(),
updatedAt: updatedAt(),
categories: relationTo(category),
}));
// define model lifecycle
const lifecycle = useLifecycle<typeof attributes>({
beforeCreated(request) {
if (!request.slug) {
throw new Error("Slug required");
}
delete request.isFeatured;
return request;
},
async afterCreated(entity) {
// send entity to algolia ...
// await algolia.create(entity)
},
});
return {
meta,
lifecycle,
...attributes,
};
});
// create backend api
const strapi = createStrapi();
strapi.use(category);
strapi.use(product);
// start strapi
strapi.start();
Content Type attribute defintion syntax
This PR introduces some changes to the content type definition files.
You can read it here