mirror of
https://github.com/strapi/strapi.git
synced 2025-11-03 11:25:17 +00:00
Update generator. Use typescript compiler API to generate definitions
This commit is contained in:
parent
44d216a028
commit
6c9b500586
@ -221,7 +221,7 @@ program
|
|||||||
'Specify a relative directory in which the schemas definitions will be generated'
|
'Specify a relative directory in which the schemas definitions will be generated'
|
||||||
)
|
)
|
||||||
.option('-f, --file <file>', 'Specify a filename to store the schemas definitions')
|
.option('-f, --file <file>', 'Specify a filename to store the schemas definitions')
|
||||||
.option('-s, --silence', `Don't display debug information`, false)
|
.option('-d, --debug', `Display debug information`, false)
|
||||||
.action(getLocalScript('content-types/generate-types'));
|
.action(getLocalScript('content-types/generate-types'));
|
||||||
|
|
||||||
program
|
program
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const tsUtils = require('@strapi/typescript-utils');
|
||||||
|
|
||||||
|
const strapi = require('../../index');
|
||||||
|
|
||||||
|
module.exports = async function({ outDir, file, debug }) {
|
||||||
|
const appContext = await strapi.compile();
|
||||||
|
const app = await strapi(appContext).register();
|
||||||
|
|
||||||
|
await tsUtils.generators.generateSchemasDefinitions({
|
||||||
|
strapi: app,
|
||||||
|
outDir: outDir || appContext.appDir,
|
||||||
|
file,
|
||||||
|
dirs: appContext,
|
||||||
|
debug,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.destroy();
|
||||||
|
};
|
||||||
@ -1,145 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const fp = require('lodash/fp');
|
|
||||||
|
|
||||||
const { addImport } = require('./imports');
|
|
||||||
const { logWarning, toType } = require('./utils');
|
|
||||||
|
|
||||||
const generateAttributesDefinition = (attributes, uid) => {
|
|
||||||
const attributesDefinitions = [];
|
|
||||||
|
|
||||||
for (const [attributeName, attribute] of Object.entries(attributes)) {
|
|
||||||
const type = getAttributeType(attributeName, attribute, uid);
|
|
||||||
|
|
||||||
attributesDefinitions.push([attributeName, type]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedDefinitions = attributesDefinitions
|
|
||||||
.map(([name, attributeType]) => ` ${name}: ${attributeType};`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
return ` attributes: {
|
|
||||||
${formattedDefinitions}
|
|
||||||
}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAttributeType = (attributeName, attribute, uid) => {
|
|
||||||
const mappers = {
|
|
||||||
string() {
|
|
||||||
return ['StringAttribute', null];
|
|
||||||
},
|
|
||||||
text() {
|
|
||||||
return ['TextAttribute', null];
|
|
||||||
},
|
|
||||||
richtext() {
|
|
||||||
return ['RichTextAttribute', null];
|
|
||||||
},
|
|
||||||
password() {
|
|
||||||
return ['PasswordAttribute', null];
|
|
||||||
},
|
|
||||||
email() {
|
|
||||||
return ['EmailAttribute', null];
|
|
||||||
},
|
|
||||||
date() {
|
|
||||||
return ['DateAttribute', null];
|
|
||||||
},
|
|
||||||
time() {
|
|
||||||
return ['TimeAttribute', null];
|
|
||||||
},
|
|
||||||
datetime() {
|
|
||||||
return ['DateTimeAttribute', null];
|
|
||||||
},
|
|
||||||
timestamp() {
|
|
||||||
return ['TimestampAttribute', null];
|
|
||||||
},
|
|
||||||
integer() {
|
|
||||||
return ['IntegerAttribute', null];
|
|
||||||
},
|
|
||||||
biginteger() {
|
|
||||||
return ['BigIntegerAttribute', null];
|
|
||||||
},
|
|
||||||
float() {
|
|
||||||
return ['FloatAttribute', null];
|
|
||||||
},
|
|
||||||
decimal() {
|
|
||||||
return ['DecimalAttribute', null];
|
|
||||||
},
|
|
||||||
uid() {
|
|
||||||
return ['UIDAttribute', null];
|
|
||||||
},
|
|
||||||
enumeration() {
|
|
||||||
return ['EnumerationAttribute', null];
|
|
||||||
},
|
|
||||||
boolean() {
|
|
||||||
return ['BooleanAttribute', null];
|
|
||||||
},
|
|
||||||
json() {
|
|
||||||
return ['JsonAttribute', null];
|
|
||||||
},
|
|
||||||
media() {
|
|
||||||
return ['MediaAttribute', null];
|
|
||||||
},
|
|
||||||
relation() {
|
|
||||||
const { relation, target } = attribute;
|
|
||||||
|
|
||||||
if (relation.includes('morph') | relation.includes('Morph')) {
|
|
||||||
return ['PolymorphicRelationAttribute', [`'${uid}'`, `'${relation}'`]];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['RelationAttribute', [`'${uid}'`, `'${relation}'`, `'${target}'`]];
|
|
||||||
},
|
|
||||||
component() {
|
|
||||||
const target = attribute.component;
|
|
||||||
const params = [`'${target}'`];
|
|
||||||
|
|
||||||
if (attribute.repeatable) {
|
|
||||||
params.push(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['ComponentAttribute', params];
|
|
||||||
},
|
|
||||||
dynamiczone() {
|
|
||||||
const components = JSON.stringify(attribute.components);
|
|
||||||
|
|
||||||
return ['DynamicZoneAttribute', [components]];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!Object.keys(mappers).includes(attribute.type)) {
|
|
||||||
logWarning(
|
|
||||||
`"${attributeName}" attribute from "${uid}" has an invalid type: "${attribute.type}"`
|
|
||||||
);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let [attributeType, typeParams] = mappers[attribute.type]();
|
|
||||||
|
|
||||||
addImport(attributeType);
|
|
||||||
|
|
||||||
let type = typeParams ? `${attributeType}<${typeParams.join(', ')}>` : attributeType;
|
|
||||||
|
|
||||||
if (attribute.required) {
|
|
||||||
addImport('RequiredAttribute');
|
|
||||||
|
|
||||||
type = `${type} & RequiredAttribute`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attribute.pluginOptions && !fp.isEmpty(attribute.pluginOptions)) {
|
|
||||||
addImport('PluginOptionsAttribute');
|
|
||||||
|
|
||||||
const pluginOptionsType = toType(attribute.pluginOptions, {
|
|
||||||
inline: true,
|
|
||||||
indent: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
type = `${type} & PluginOptionsAttribute<${pluginOptionsType}>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return type;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
generateAttributesDefinition,
|
|
||||||
getAttributeType,
|
|
||||||
};
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const generateGlobalDefinition = definitions => {
|
|
||||||
const formattedSchemasDefinitions = definitions
|
|
||||||
.map(({ uid, type }) => ` '${uid}': ${type}`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
return `
|
|
||||||
declare global {
|
|
||||||
namespace Strapi {
|
|
||||||
interface Schemas {
|
|
||||||
${formattedSchemasDefinitions}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
generateGlobalDefinition,
|
|
||||||
};
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const imports = [];
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getImports() {
|
|
||||||
return imports;
|
|
||||||
},
|
|
||||||
|
|
||||||
addImport(type) {
|
|
||||||
const hasType = imports.includes(type);
|
|
||||||
|
|
||||||
if (!hasType) {
|
|
||||||
imports.push(type);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
generateImports() {
|
|
||||||
const formattedImports = imports.map(p => ` ${p}`).join(',\n');
|
|
||||||
|
|
||||||
return `import {
|
|
||||||
${formattedImports}
|
|
||||||
} from '@strapi/strapi';
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,167 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const CLITable = require('cli-table3');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const fp = require('lodash/fp');
|
|
||||||
const fse = require('fs-extra');
|
|
||||||
|
|
||||||
const tsUtils = require('@strapi/typescript-utils');
|
|
||||||
|
|
||||||
const createStrapiInstance = require('../../../index');
|
|
||||||
|
|
||||||
const { generateSchemaDefinition } = require('./schemas');
|
|
||||||
const { generateGlobalDefinition } = require('./global');
|
|
||||||
const { generateImports } = require('./imports');
|
|
||||||
const { logWarning, getSchemaTypeName } = require('./utils');
|
|
||||||
|
|
||||||
module.exports = async function({ outDir, file, silence }) {
|
|
||||||
const [app, dirs] = await setup();
|
|
||||||
|
|
||||||
const schemas = getAllStrapiSchemas(app);
|
|
||||||
|
|
||||||
const table = createInfoTable();
|
|
||||||
|
|
||||||
const definitions = generateTypesDefinitions(schemas);
|
|
||||||
const globalDefinition = generateGlobalDefinition(definitions);
|
|
||||||
const imports = generateImports();
|
|
||||||
|
|
||||||
const fullDefinition = [
|
|
||||||
imports + '\n',
|
|
||||||
definitions.map(fp.get('definition')).join('\n'),
|
|
||||||
globalDefinition,
|
|
||||||
].join('');
|
|
||||||
|
|
||||||
await generateSchemaFile(outDir || dirs.app, fullDefinition, file);
|
|
||||||
|
|
||||||
for (const definition of definitions) {
|
|
||||||
const isValidDefinition = definition.definition !== null;
|
|
||||||
const validateAndTransform = isValidDefinition ? fp.identity : chalk.redBright;
|
|
||||||
|
|
||||||
table.push([
|
|
||||||
validateAndTransform(definition.kind),
|
|
||||||
validateAndTransform(definition.uid),
|
|
||||||
validateAndTransform(definition.type),
|
|
||||||
isValidDefinition ? chalk.greenBright('✓') : chalk.redBright('✗'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const successfullDefinition = fp.filter(d => !fp.isNil(d.definition), definitions);
|
|
||||||
const skippedDefinition = fp.filter(d => fp.isNil(d.definition), definitions);
|
|
||||||
|
|
||||||
if (!silence) {
|
|
||||||
console.log(table.toString());
|
|
||||||
console.log(
|
|
||||||
chalk.greenBright(
|
|
||||||
`Generated ${fp.size(
|
|
||||||
successfullDefinition
|
|
||||||
)} type definition for your Strapi application's schemas.`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const skippedAmount = fp.size(skippedDefinition);
|
|
||||||
|
|
||||||
if (skippedAmount > 0) {
|
|
||||||
console.log(
|
|
||||||
chalk.redBright(
|
|
||||||
`Skipped ${skippedAmount} (${skippedDefinition.map(d => d.uid).join(', ')})`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.destroy();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup a Strapi application based on the current process directory
|
|
||||||
* @returns {Promise<[Strapi, { app: string, dist: string }]>}
|
|
||||||
*/
|
|
||||||
const setup = async () => {
|
|
||||||
const dirs = { app: process.cwd(), dist: process.cwd() };
|
|
||||||
|
|
||||||
const isTSProject = await tsUtils.isUsingTypeScript(dirs.app);
|
|
||||||
|
|
||||||
if (isTSProject) {
|
|
||||||
await tsUtils.compile(dirs.app, { configOptions: { options: { incremental: true } } });
|
|
||||||
|
|
||||||
dirs.dist = await tsUtils.resolveOutDir(dirs.app);
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = await createStrapiInstance({ appDir: dirs.app, distDir: dirs.dist }).register();
|
|
||||||
|
|
||||||
return [app, dirs];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate all the TypeScript definitions for the app schemas
|
|
||||||
* @param {Object} schemas
|
|
||||||
*/
|
|
||||||
const generateTypesDefinitions = schemas => {
|
|
||||||
const definitions = [];
|
|
||||||
|
|
||||||
for (const [uid, schema] of Object.entries(schemas)) {
|
|
||||||
const { modelType, kind } = schema;
|
|
||||||
// Schema UID -> Interface Name
|
|
||||||
const type = getSchemaTypeName(uid);
|
|
||||||
|
|
||||||
let definition;
|
|
||||||
|
|
||||||
const isComponent = modelType === 'component';
|
|
||||||
const isContentType =
|
|
||||||
modelType === 'contentType' && ['singleType', 'collectionType'].includes(kind);
|
|
||||||
|
|
||||||
// Handle components and content types
|
|
||||||
if (isComponent || isContentType) {
|
|
||||||
definition = generateSchemaDefinition(uid, schema, type) + '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other
|
|
||||||
else {
|
|
||||||
logWarning(`"${uid}" has an invalid model definition. Skipping...`);
|
|
||||||
definition = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the generated definition to the list
|
|
||||||
definitions.push({
|
|
||||||
type,
|
|
||||||
uid,
|
|
||||||
schema,
|
|
||||||
definition,
|
|
||||||
kind: fp.upperFirst(modelType === 'component' ? 'component' : kind),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return definitions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the content-types definitions file based on the given arguments
|
|
||||||
* @param {string} dir
|
|
||||||
* @param {string} definition
|
|
||||||
* @param {string} [file]
|
|
||||||
*/
|
|
||||||
const generateSchemaFile = async (dir, definition, file) => {
|
|
||||||
const filePath = path.join(dir, file || 'schemas.d.ts');
|
|
||||||
|
|
||||||
await fse.writeFile(filePath, definition);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all content types and components loaded in the Strapi instance
|
|
||||||
*
|
|
||||||
* @param {Strapi} app
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
const getAllStrapiSchemas = app => ({ ...app.contentTypes, ...app.components });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new info table for the content types definitions
|
|
||||||
*/
|
|
||||||
const createInfoTable = () => {
|
|
||||||
return new CLITable({
|
|
||||||
head: [chalk.green('Model Type'), chalk.blue('UID'), chalk.blue('Type'), chalk.gray('Status')],
|
|
||||||
colAligns: ['center', 'left', 'left', 'center'],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const fp = require('lodash/fp');
|
|
||||||
|
|
||||||
const { generateAttributesDefinition } = require('./attributes');
|
|
||||||
const { addImport } = require('./imports');
|
|
||||||
const { toType } = require('./utils');
|
|
||||||
|
|
||||||
const getBaseSchema = schema => {
|
|
||||||
const { modelType, kind } = schema;
|
|
||||||
|
|
||||||
// Component
|
|
||||||
if (modelType === 'component') {
|
|
||||||
return 'ComponentSchema';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content Type
|
|
||||||
else if (modelType === 'contentType') {
|
|
||||||
return `${fp.upperFirst(kind)}Schema`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFormatOptions = prefix => ({ indentStart: 2, suffix: ';', prefix });
|
|
||||||
|
|
||||||
const generateSchemaDefinition = (uid, schema, type) => {
|
|
||||||
const baseInterface = getBaseSchema(schema);
|
|
||||||
|
|
||||||
addImport(baseInterface);
|
|
||||||
|
|
||||||
const propertiesKeys = ['info', 'options', 'pluginOptions'];
|
|
||||||
|
|
||||||
const definitionBody = propertiesKeys
|
|
||||||
.map(key => toType(fp.get(key, schema), getFormatOptions(key)))
|
|
||||||
.concat(generateAttributesDefinition(schema.attributes, uid))
|
|
||||||
.filter(def => !fp.isNil(def))
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
return `interface ${type} extends ${baseInterface} {\n${definitionBody}\n}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
generateSchemaDefinition,
|
|
||||||
};
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const fp = require('lodash/fp');
|
|
||||||
|
|
||||||
const logWarning = message => {
|
|
||||||
console.log(chalk.yellow(`[${new Date().toLocaleTimeString()}] (warning):\t${message}`));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSchemaTypeName = fp.flow(fp.replace(/(:.)/, ' '), fp.camelCase, fp.upperFirst);
|
|
||||||
|
|
||||||
const toType = (object, formatOptions = {}) => {
|
|
||||||
const {
|
|
||||||
prefix = null,
|
|
||||||
suffix = null,
|
|
||||||
inline = false,
|
|
||||||
indent = 2,
|
|
||||||
indentStart = 0,
|
|
||||||
} = formatOptions;
|
|
||||||
|
|
||||||
if (fp.isNil(object) || fp.isEmpty(object)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let definition = '';
|
|
||||||
|
|
||||||
const properties = Object.entries(object);
|
|
||||||
|
|
||||||
const lineBreak = (s = '') => (inline ? s : '\n');
|
|
||||||
const offset = (m = 0) => ' '.repeat(indentStart + indent * m);
|
|
||||||
const getPrefix = (s = '') => (prefix ? `${prefix}: ` : s);
|
|
||||||
const getSuffix = (s = '') => suffix || s;
|
|
||||||
|
|
||||||
for (const [key, value] of properties) {
|
|
||||||
const validKey = key.includes('-') ? `'${key}'` : key;
|
|
||||||
|
|
||||||
let row;
|
|
||||||
|
|
||||||
// TODO: Handle arrays types
|
|
||||||
|
|
||||||
// Handle recursive types (objects)
|
|
||||||
if (fp.isObject(value) && !fp.isEmpty(value)) {
|
|
||||||
row = toType(value, {
|
|
||||||
prefix: validKey,
|
|
||||||
indentStart: indentStart + indent,
|
|
||||||
suffix,
|
|
||||||
inline,
|
|
||||||
indent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// All non-recursive types are handled there
|
|
||||||
else {
|
|
||||||
let type = value;
|
|
||||||
|
|
||||||
if (fp.isString(value)) {
|
|
||||||
type = `'${value}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
row = `${offset(1)}${validKey}: ${type};${lineBreak(' ')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
definition += row;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!inline) {
|
|
||||||
definition = definition.slice(0, definition.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${offset()}${getPrefix()}{${lineBreak(' ')}${definition}${lineBreak(
|
|
||||||
''
|
|
||||||
)}${offset()}}${getSuffix()}${lineBreak()}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
logWarning,
|
|
||||||
getSchemaTypeName,
|
|
||||||
toType,
|
|
||||||
};
|
|
||||||
@ -36,7 +36,7 @@ export type SetMinMax<T extends MinMaxOption<U>, U = number> = T;
|
|||||||
export type SetMinMaxLength<T extends MinMaxLengthOption> = T;
|
export type SetMinMaxLength<T extends MinMaxLengthOption> = T;
|
||||||
|
|
||||||
// pluginOptions
|
// pluginOptions
|
||||||
export type SetAttributePluginOptions<T extends object = object> = { pluginOptions?: T };
|
export type SetPluginOptions<T extends object = object> = { pluginOptions?: T };
|
||||||
|
|
||||||
// default
|
// default
|
||||||
export type DefaultTo<T> = { default: T };
|
export type DefaultTo<T> = { default: T };
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export interface PolymorphicRelationAttributeProperties<
|
|||||||
export type RelationAttribute<
|
export type RelationAttribute<
|
||||||
S extends SchemaUID,
|
S extends SchemaUID,
|
||||||
R extends RelationsType,
|
R extends RelationsType,
|
||||||
T extends SchemaUID
|
T extends R extends PolymorphicRelationsType ? never: SchemaUID = never
|
||||||
> = Attribute<'relation'> &
|
> = Attribute<'relation'> &
|
||||||
// Properties
|
// Properties
|
||||||
(R extends BasicRelationsType
|
(R extends BasicRelationsType
|
||||||
|
|||||||
7
packages/utils/typescript/lib/generators/index.js
Normal file
7
packages/utils/typescript/lib/generators/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const generateSchemasDefinitions = require('./schemas');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateSchemasDefinitions,
|
||||||
|
};
|
||||||
241
packages/utils/typescript/lib/generators/schemas/attributes.js
Normal file
241
packages/utils/typescript/lib/generators/schemas/attributes.js
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { factory } = require('typescript');
|
||||||
|
const fp = require('lodash/fp');
|
||||||
|
|
||||||
|
const { addImport } = require('./imports');
|
||||||
|
const { getTypeNode, toTypeLitteral } = require('./utils');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a property signature node for a given attribute
|
||||||
|
*
|
||||||
|
* @param {object} schema
|
||||||
|
* @param {string} attributeName
|
||||||
|
* @param {object} attribute
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
const attributeToPropertySignature = (schema, attributeName, attribute) => {
|
||||||
|
const baseType = getAttributeType(attributeName, attribute, schema.uid);
|
||||||
|
const modifiers = getAttributeModifiers(attributeName, attribute);
|
||||||
|
|
||||||
|
const nodes = [baseType, ...modifiers];
|
||||||
|
|
||||||
|
return factory.createPropertySignature(
|
||||||
|
undefined,
|
||||||
|
factory.createIdentifier(attributeName),
|
||||||
|
undefined,
|
||||||
|
factory.createIntersectionTypeNode(nodes)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the base type node for a given attribute
|
||||||
|
*
|
||||||
|
* @param {string} attributeName
|
||||||
|
* @param {object} attribute
|
||||||
|
* @param {string} uid
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
const getAttributeType = (attributeName, attribute, uid) => {
|
||||||
|
const mappers = {
|
||||||
|
string() {
|
||||||
|
return ['StringAttribute'];
|
||||||
|
},
|
||||||
|
text() {
|
||||||
|
return ['TextAttribute'];
|
||||||
|
},
|
||||||
|
richtext() {
|
||||||
|
return ['RichTextAttribute'];
|
||||||
|
},
|
||||||
|
password() {
|
||||||
|
return ['PasswordAttribute'];
|
||||||
|
},
|
||||||
|
email() {
|
||||||
|
return ['EmailAttribute'];
|
||||||
|
},
|
||||||
|
date() {
|
||||||
|
return ['DateAttribute'];
|
||||||
|
},
|
||||||
|
time() {
|
||||||
|
return ['TimeAttribute'];
|
||||||
|
},
|
||||||
|
datetime() {
|
||||||
|
return ['DateTimeAttribute'];
|
||||||
|
},
|
||||||
|
timestamp() {
|
||||||
|
return ['TimestampAttribute'];
|
||||||
|
},
|
||||||
|
integer() {
|
||||||
|
return ['IntegerAttribute'];
|
||||||
|
},
|
||||||
|
biginteger() {
|
||||||
|
return ['BigIntegerAttribute'];
|
||||||
|
},
|
||||||
|
float() {
|
||||||
|
return ['FloatAttribute'];
|
||||||
|
},
|
||||||
|
decimal() {
|
||||||
|
return ['DecimalAttribute'];
|
||||||
|
},
|
||||||
|
uid() {
|
||||||
|
return ['UIDAttribute'];
|
||||||
|
},
|
||||||
|
enumeration() {
|
||||||
|
return ['EnumerationAttribute'];
|
||||||
|
},
|
||||||
|
boolean() {
|
||||||
|
return ['BooleanAttribute'];
|
||||||
|
},
|
||||||
|
json() {
|
||||||
|
return ['JSONAttribute'];
|
||||||
|
},
|
||||||
|
media() {
|
||||||
|
return ['MediaAttribute'];
|
||||||
|
},
|
||||||
|
relation() {
|
||||||
|
const { relation, target } = attribute;
|
||||||
|
|
||||||
|
if (relation.includes('morph') | relation.includes('Morph')) {
|
||||||
|
return [
|
||||||
|
'RelationAttribute',
|
||||||
|
[factory.createStringLiteral(uid, true), factory.createStringLiteral(relation, true)],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'RelationAttribute',
|
||||||
|
[
|
||||||
|
factory.createStringLiteral(uid, true),
|
||||||
|
factory.createStringLiteral(relation, true),
|
||||||
|
factory.createStringLiteral(target, true),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
},
|
||||||
|
component() {
|
||||||
|
const target = attribute.component;
|
||||||
|
const params = [factory.createStringLiteral(target, true)];
|
||||||
|
|
||||||
|
if (attribute.repeatable) {
|
||||||
|
params.push(factory.createTrue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['ComponentAttribute', params];
|
||||||
|
},
|
||||||
|
dynamiczone() {
|
||||||
|
const componentsParam = factory.createTupleTypeNode(
|
||||||
|
attribute.components.map(component => factory.createStringLiteral(component))
|
||||||
|
);
|
||||||
|
|
||||||
|
return ['DynamicZoneAttribute', [componentsParam]];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!Object.keys(mappers).includes(attribute.type)) {
|
||||||
|
console.warning(
|
||||||
|
`"${attributeName}" attribute from "${uid}" has an invalid type: "${attribute.type}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [attributeType, typeParams] = mappers[attribute.type]();
|
||||||
|
|
||||||
|
addImport(attributeType);
|
||||||
|
|
||||||
|
return getTypeNode(attributeType, typeParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect every modifier node from an attribute
|
||||||
|
*
|
||||||
|
* @param {string} _attributeName
|
||||||
|
* @param {object} attribute
|
||||||
|
* @returns {object[]}
|
||||||
|
*/
|
||||||
|
const getAttributeModifiers = (_attributeName, attribute) => {
|
||||||
|
const modifiers = [];
|
||||||
|
|
||||||
|
// Required
|
||||||
|
if (attribute.required) {
|
||||||
|
addImport('RequiredAttribute');
|
||||||
|
|
||||||
|
modifiers.push(factory.createTypeReferenceNode(factory.createIdentifier('RequiredAttribute')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
if (attribute.private) {
|
||||||
|
addImport('PrivateAttribute');
|
||||||
|
|
||||||
|
modifiers.push(factory.createTypeReferenceNode(factory.createIdentifier('PrivateAttribute')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unique
|
||||||
|
if (attribute.unique) {
|
||||||
|
addImport('UniqueAttribute');
|
||||||
|
|
||||||
|
modifiers.push(factory.createTypeReferenceNode(factory.createIdentifier('UniqueAttribute')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configurable
|
||||||
|
if (attribute.configurable) {
|
||||||
|
addImport('ConfigurableAttribute');
|
||||||
|
|
||||||
|
modifiers.push(
|
||||||
|
factory.createTypeReferenceNode(factory.createIdentifier('ConfigurableAttribute'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin Options
|
||||||
|
if (!fp.isEmpty(attribute.pluginOptions)) {
|
||||||
|
addImport('SetPluginOptions');
|
||||||
|
|
||||||
|
modifiers.push(
|
||||||
|
factory.createTypeReferenceNode(
|
||||||
|
factory.createIdentifier('SetPluginOptions'),
|
||||||
|
// Transform the pluginOptions object into an object litteral expression
|
||||||
|
[toTypeLitteral(attribute.pluginOptions)]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min / Max
|
||||||
|
if (!fp.isNil(attribute.min) || !fp.isNil(attribute.max)) {
|
||||||
|
addImport('SetMinMax');
|
||||||
|
|
||||||
|
const minMaxProperties = fp.pick(['min', 'max'], attribute);
|
||||||
|
|
||||||
|
modifiers.push(
|
||||||
|
factory.createTypeReferenceNode(factory.createIdentifier('SetMinMax'), [
|
||||||
|
toTypeLitteral(minMaxProperties),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min length / Max length
|
||||||
|
if (!fp.isNil(attribute.minLength) || !fp.isNil(attribute.maxLength)) {
|
||||||
|
addImport('SetMinMaxLength');
|
||||||
|
|
||||||
|
const minMaxProperties = fp.pick(['minLength', 'maxLength'], attribute);
|
||||||
|
|
||||||
|
modifiers.push(
|
||||||
|
factory.createTypeReferenceNode(factory.createIdentifier('SetMinMaxLength'), [
|
||||||
|
toTypeLitteral(minMaxProperties),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default
|
||||||
|
if (!fp.isNil(attribute.default)) {
|
||||||
|
addImport('DefaultTo');
|
||||||
|
|
||||||
|
const defaultLitteral = toTypeLitteral(attribute.default);
|
||||||
|
|
||||||
|
modifiers.push(
|
||||||
|
factory.createTypeReferenceNode(factory.createIdentifier('DefaultTo'), [defaultLitteral])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifiers;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = attributeToPropertySignature;
|
||||||
68
packages/utils/typescript/lib/generators/schemas/global.js
Normal file
68
packages/utils/typescript/lib/generators/schemas/global.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const ts = require('typescript');
|
||||||
|
const { factory } = require('typescript');
|
||||||
|
|
||||||
|
const { getSchemaInterfaceName } = require('./utils');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the global module augmentation block
|
||||||
|
*
|
||||||
|
* @param {Array<{ schema: object; definition: ts.TypeNode }>} schemasDefinitions
|
||||||
|
* @returns {ts.ModuleDeclaration}
|
||||||
|
*/
|
||||||
|
const generateGlobalDefinition = (schemasDefinitions = []) => {
|
||||||
|
const properties = schemasDefinitions.map(schemaDefinitionToPropertySignature);
|
||||||
|
|
||||||
|
return factory.createModuleDeclaration(
|
||||||
|
undefined,
|
||||||
|
[factory.createModifier(ts.SyntaxKind.DeclareKeyword)],
|
||||||
|
factory.createIdentifier('global'),
|
||||||
|
factory.createModuleBlock([
|
||||||
|
factory.createModuleDeclaration(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
factory.createIdentifier('Strapi'),
|
||||||
|
factory.createModuleBlock([
|
||||||
|
factory.createInterfaceDeclaration(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
factory.createIdentifier('Schemas'),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
properties
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
ts.NodeFlags.Namespace |
|
||||||
|
ts.NodeFlags.ExportContext |
|
||||||
|
ts.NodeFlags.Ambient |
|
||||||
|
ts.NodeFlags.ContextFlags
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
ts.NodeFlags.ExportContext |
|
||||||
|
ts.NodeFlags.GlobalAugmentation |
|
||||||
|
ts.NodeFlags.Ambient |
|
||||||
|
ts.NodeFlags.ContextFlags
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} schemaDefinition
|
||||||
|
* @param {ts.InterfaceDeclaration} schemaDefinition.definition
|
||||||
|
* @param {object} schemaDefinition.schema
|
||||||
|
*/
|
||||||
|
const schemaDefinitionToPropertySignature = ({ schema }) => {
|
||||||
|
const { uid } = schema;
|
||||||
|
|
||||||
|
const interfaceTypeName = getSchemaInterfaceName(uid);
|
||||||
|
|
||||||
|
return factory.createPropertySignature(
|
||||||
|
undefined,
|
||||||
|
factory.createStringLiteral(uid, true),
|
||||||
|
undefined,
|
||||||
|
factory.createTypeReferenceNode(factory.createIdentifier(interfaceTypeName))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { generateGlobalDefinition };
|
||||||
33
packages/utils/typescript/lib/generators/schemas/imports.js
Normal file
33
packages/utils/typescript/lib/generators/schemas/imports.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { factory } = require('typescript');
|
||||||
|
|
||||||
|
const imports = [];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getImports() {
|
||||||
|
return imports;
|
||||||
|
},
|
||||||
|
|
||||||
|
addImport(type) {
|
||||||
|
const hasType = imports.includes(type);
|
||||||
|
|
||||||
|
if (!hasType) {
|
||||||
|
imports.push(type);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
generateImportDefinition() {
|
||||||
|
const formattedImports = imports.map(key =>
|
||||||
|
factory.createImportSpecifier(false, undefined, factory.createIdentifier(key))
|
||||||
|
);
|
||||||
|
|
||||||
|
return factory.createImportDeclaration(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
factory.createImportClause(false, undefined, factory.createNamedImports(formattedImports)),
|
||||||
|
factory.createStringLiteral('@strapi/strapi'),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
173
packages/utils/typescript/lib/generators/schemas/index.js
Normal file
173
packages/utils/typescript/lib/generators/schemas/index.js
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const ts = require('typescript');
|
||||||
|
const { factory } = require('typescript');
|
||||||
|
|
||||||
|
const fp = require('lodash/fp');
|
||||||
|
const fse = require('fs-extra');
|
||||||
|
const prettier = require('prettier');
|
||||||
|
const chalk = require('chalk');
|
||||||
|
const CLITable = require('cli-table3');
|
||||||
|
|
||||||
|
const { generateImportDefinition } = require('./imports');
|
||||||
|
const { generateSchemaDefinition } = require('./schema');
|
||||||
|
const { generateGlobalDefinition } = require('./global');
|
||||||
|
const {
|
||||||
|
getAllStrapiSchemas,
|
||||||
|
getSchemaInterfaceName,
|
||||||
|
getSchemaModelType,
|
||||||
|
getDefinitionAttributesCount,
|
||||||
|
} = require('./utils');
|
||||||
|
|
||||||
|
const DEFAULT_OUT_FILENAME = 'schemas.d.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate type definitions for Strapi schemas
|
||||||
|
*
|
||||||
|
* @param {object} options
|
||||||
|
* @param {Strapi} options.strapi
|
||||||
|
* @param {{ distDir: string; appDir: string; }} options.dirs
|
||||||
|
* @param {string} [options.outDir]
|
||||||
|
* @param {string} [options.file]
|
||||||
|
* @param {boolean} [options.debug]
|
||||||
|
*/
|
||||||
|
const generateSchemasDefinitions = async (options = {}) => {
|
||||||
|
const { strapi, outDir = process.cwd(), file = DEFAULT_OUT_FILENAME, debug = true } = options;
|
||||||
|
|
||||||
|
const schemas = getAllStrapiSchemas(strapi);
|
||||||
|
|
||||||
|
const schemasDefinitions = Object.values(schemas).map(schema => ({
|
||||||
|
schema,
|
||||||
|
definition: generateSchemaDefinition(schema),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const allDefinitions = [
|
||||||
|
// Imports
|
||||||
|
generateImportDefinition(),
|
||||||
|
|
||||||
|
// Add a newline after the import statement
|
||||||
|
factory.createIdentifier('\n'),
|
||||||
|
|
||||||
|
// Schemas
|
||||||
|
...schemasDefinitions.reduce(
|
||||||
|
(acc, def) => [
|
||||||
|
...acc,
|
||||||
|
def.definition,
|
||||||
|
// Add a newline between each interface declaration
|
||||||
|
factory.createIdentifier('\n'),
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
|
||||||
|
// Global
|
||||||
|
generateGlobalDefinition(schemasDefinitions),
|
||||||
|
];
|
||||||
|
|
||||||
|
const output = emitDefinitions(allDefinitions);
|
||||||
|
const formattedOutput = await format(output);
|
||||||
|
|
||||||
|
const definitionFilepath = await saveDefinitionToFileSystem(outDir, file, formattedOutput);
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
logDebugInformation(schemasDefinitions, { filepath: definitionFilepath });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitDefinitions = definitions => {
|
||||||
|
const nodeArray = factory.createNodeArray(definitions);
|
||||||
|
|
||||||
|
const sourceFile = ts.createSourceFile(
|
||||||
|
'placeholder.ts',
|
||||||
|
'',
|
||||||
|
ts.ScriptTarget.ESNext,
|
||||||
|
true,
|
||||||
|
ts.ScriptKind.TS
|
||||||
|
);
|
||||||
|
|
||||||
|
const printer = ts.createPrinter({ newLine: true, omitTrailingSemicolon: true });
|
||||||
|
|
||||||
|
return printer.printList(ts.ListFormat.MultiLine, nodeArray, sourceFile);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveDefinitionToFileSystem = async (dir, file, content) => {
|
||||||
|
const filepath = path.join(dir, file);
|
||||||
|
|
||||||
|
await fse.writeFile(filepath, content);
|
||||||
|
|
||||||
|
return filepath;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the given definitions.
|
||||||
|
* Uses the existing config if one is defined in the project.
|
||||||
|
*
|
||||||
|
* @param {string} content
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const format = async content => {
|
||||||
|
const configFile = await prettier.resolveConfigFile();
|
||||||
|
const config = configFile
|
||||||
|
? await prettier.resolveConfig(configFile)
|
||||||
|
: // Default config
|
||||||
|
{
|
||||||
|
singleQuote: true,
|
||||||
|
useTabs: false,
|
||||||
|
tabWidth: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(config, { parser: 'typescript' });
|
||||||
|
|
||||||
|
return prettier.format(content, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logDebugInformation = (definitions, options = {}) => {
|
||||||
|
const { filepath } = options;
|
||||||
|
|
||||||
|
const table = new CLITable({
|
||||||
|
head: [
|
||||||
|
chalk.bold(chalk.green('Model Type')),
|
||||||
|
chalk.bold(chalk.blue('UID')),
|
||||||
|
chalk.bold(chalk.blue('Type')),
|
||||||
|
chalk.bold(chalk.gray('Attributes Count')),
|
||||||
|
],
|
||||||
|
colAligns: ['center', 'left', 'left', 'center'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedDefinitions = definitions.map(def => ({
|
||||||
|
...def,
|
||||||
|
attributesCount: getDefinitionAttributesCount(def.definition),
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const { schema, attributesCount } of sortedDefinitions) {
|
||||||
|
const modelType = fp.upperFirst(getSchemaModelType(schema));
|
||||||
|
const interfaceType = getSchemaInterfaceName(schema.uid);
|
||||||
|
|
||||||
|
table.push([
|
||||||
|
chalk.greenBright(modelType),
|
||||||
|
chalk.blue(schema.uid),
|
||||||
|
chalk.blue(interfaceType),
|
||||||
|
chalk.grey(fp.isNil(attributesCount) ? 'N/A' : attributesCount),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table
|
||||||
|
console.log(table.toString());
|
||||||
|
|
||||||
|
// Metrics
|
||||||
|
console.log(
|
||||||
|
chalk.greenBright(
|
||||||
|
`Generated ${definitions.length} type definition for your Strapi application's schemas.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filepath
|
||||||
|
const relativePath = path.relative(process.cwd(), filepath);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
chalk.grey(`The definitions file has been generated here: ${chalk.bold(relativePath)}`)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = generateSchemasDefinitions;
|
||||||
87
packages/utils/typescript/lib/generators/schemas/schema.js
Normal file
87
packages/utils/typescript/lib/generators/schemas/schema.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const ts = require('typescript');
|
||||||
|
const { factory } = require('typescript');
|
||||||
|
const { isEmpty } = require('lodash/fp');
|
||||||
|
|
||||||
|
const { getSchemaExtendsTypeName, getSchemaInterfaceName, toTypeLitteral } = require('./utils');
|
||||||
|
const attributeToPropertySignature = require('./attributes');
|
||||||
|
const { addImport } = require('./imports');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an interface declaration for a given schema
|
||||||
|
*
|
||||||
|
* @param {object} schema
|
||||||
|
* @returns {ts.InterfaceDeclaration}
|
||||||
|
*/
|
||||||
|
const generateSchemaDefinition = schema => {
|
||||||
|
const { uid } = schema;
|
||||||
|
|
||||||
|
// Resolve the different interface names needed to declare the schema's interface
|
||||||
|
const interfaceName = getSchemaInterfaceName(uid);
|
||||||
|
const parentType = getSchemaExtendsTypeName(schema);
|
||||||
|
|
||||||
|
// Make sure the extended interface are imported
|
||||||
|
addImport(parentType);
|
||||||
|
|
||||||
|
// Properties whose values can be mapped to a litteral type expression
|
||||||
|
const litteralPropertiesDefinitions = ['info', 'options', 'pluginOptions']
|
||||||
|
// Ignore non-existent or empty declarations
|
||||||
|
.filter(key => !isEmpty(schema[key]))
|
||||||
|
// Generate litteral definition for each property
|
||||||
|
.map(generatePropertyLitteralDefinitionFactory(schema));
|
||||||
|
|
||||||
|
// Generate the `attributes` litteral type definition
|
||||||
|
const attributesProp = generateAttributePropertySignature(schema);
|
||||||
|
|
||||||
|
// Merge every schema's definition in a single list
|
||||||
|
const schemaProperties = [...litteralPropertiesDefinitions, attributesProp];
|
||||||
|
|
||||||
|
// Generate the schema's interface declaration
|
||||||
|
const schemaType = factory.createInterfaceDeclaration(
|
||||||
|
undefined,
|
||||||
|
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
|
||||||
|
factory.createIdentifier(interfaceName),
|
||||||
|
undefined,
|
||||||
|
[
|
||||||
|
factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [
|
||||||
|
factory.createIdentifier(parentType),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
schemaProperties
|
||||||
|
);
|
||||||
|
|
||||||
|
return schemaType;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a property signature for the schema's `attributes` field
|
||||||
|
*
|
||||||
|
* @param {object} schema
|
||||||
|
* @returns {ts.PropertySignature}
|
||||||
|
*/
|
||||||
|
const generateAttributePropertySignature = schema => {
|
||||||
|
const { attributes } = schema;
|
||||||
|
|
||||||
|
const properties = Object.entries(attributes).map(([attributeName, attribute]) => {
|
||||||
|
return attributeToPropertySignature(schema, attributeName, attribute);
|
||||||
|
});
|
||||||
|
|
||||||
|
return factory.createPropertySignature(
|
||||||
|
undefined,
|
||||||
|
factory.createIdentifier('attributes'),
|
||||||
|
undefined,
|
||||||
|
factory.createTypeLiteralNode(properties)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePropertyLitteralDefinitionFactory = schema => key => {
|
||||||
|
return factory.createPropertySignature(
|
||||||
|
undefined,
|
||||||
|
factory.createIdentifier(key),
|
||||||
|
undefined,
|
||||||
|
toTypeLitteral(schema[key])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { generateSchemaDefinition };
|
||||||
145
packages/utils/typescript/lib/generators/schemas/utils.js
Normal file
145
packages/utils/typescript/lib/generators/schemas/utils.js
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { factory } = require('typescript');
|
||||||
|
const {
|
||||||
|
flow,
|
||||||
|
replace,
|
||||||
|
camelCase,
|
||||||
|
upperFirst,
|
||||||
|
isObject,
|
||||||
|
isString,
|
||||||
|
isNumber,
|
||||||
|
isArray,
|
||||||
|
isBoolean,
|
||||||
|
propEq,
|
||||||
|
} = require('lodash/fp');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all components and content-types in a Strapi application
|
||||||
|
*
|
||||||
|
* @param {Strapi} strapi
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
const getAllStrapiSchemas = strapi => ({ ...strapi.contentTypes, ...strapi.components });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a valid interface name from a schema uid
|
||||||
|
*
|
||||||
|
* @param {string} uid
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const getSchemaInterfaceName = flow(replace(/(:.)/, ' '), camelCase, upperFirst);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the parent type name to extend based on the schema's nature
|
||||||
|
*
|
||||||
|
* @param {object} schema
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const getSchemaExtendsTypeName = schema => {
|
||||||
|
const base = getSchemaModelType(schema);
|
||||||
|
|
||||||
|
return upperFirst(base) + 'Schema';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSchemaModelType = schema => {
|
||||||
|
const { modelType, kind } = schema;
|
||||||
|
|
||||||
|
// Components
|
||||||
|
if (modelType === 'component') {
|
||||||
|
return 'component';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content-Types
|
||||||
|
else if (modelType === 'contentType') {
|
||||||
|
return kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a type node based on a type and its params
|
||||||
|
*
|
||||||
|
* @param {string} typeName
|
||||||
|
* @param {ts.TypeNode[]} [params]
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getTypeNode = (typeName, params = []) => {
|
||||||
|
return factory.createTypeReferenceNode(factory.createIdentifier(typeName), params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a regular JavaScript object into an object litteral expression
|
||||||
|
* @param {object} data
|
||||||
|
* @returns {ts.ObjectLiteralExpression}
|
||||||
|
*/
|
||||||
|
const toTypeLitteral = data => {
|
||||||
|
if (isString(data)) {
|
||||||
|
return factory.createStringLiteral(data, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNumber(data)) {
|
||||||
|
return factory.createNumericLiteral(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBoolean(data)) {
|
||||||
|
return data ? factory.createTrue() : factory.createFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray(data)) {
|
||||||
|
return factory.createTupleTypeNode(data.map(item => toTypeLitteral(item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isObject(data)) {
|
||||||
|
throw new Error('Cannot convert to object litteral. Unknown type', typeof data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Object.entries(data);
|
||||||
|
|
||||||
|
const props = entries.reduce((acc, [key, value]) => {
|
||||||
|
// Handle keys such as content-type-builder & co.
|
||||||
|
const identifier = key.includes('-')
|
||||||
|
? factory.createStringLiteral(key, true)
|
||||||
|
: factory.createIdentifier(key);
|
||||||
|
|
||||||
|
return [
|
||||||
|
...acc,
|
||||||
|
factory.createPropertyDeclaration(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
identifier,
|
||||||
|
undefined,
|
||||||
|
toTypeLitteral(value)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return factory.createTypeLiteralNode(props);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of attributes generated for a given schema definition
|
||||||
|
*
|
||||||
|
* @param {ts.TypeNode} definition
|
||||||
|
* @returns {number | null}
|
||||||
|
*/
|
||||||
|
const getDefinitionAttributesCount = definition => {
|
||||||
|
const attributesNode = definition.members.find(propEq('name.escapedText', 'attributes'));
|
||||||
|
|
||||||
|
if (!attributesNode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributesNode.type.members.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAllStrapiSchemas,
|
||||||
|
getSchemaInterfaceName,
|
||||||
|
getSchemaExtendsTypeName,
|
||||||
|
getSchemaModelType,
|
||||||
|
getDefinitionAttributesCount,
|
||||||
|
getTypeNode,
|
||||||
|
toTypeLitteral,
|
||||||
|
};
|
||||||
@ -4,11 +4,13 @@ const compile = require('./compile');
|
|||||||
const compilers = require('./compilers');
|
const compilers = require('./compilers');
|
||||||
const admin = require('./admin');
|
const admin = require('./admin');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
|
const generators = require('./generators');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
compile,
|
compile,
|
||||||
compilers,
|
compilers,
|
||||||
admin,
|
admin,
|
||||||
|
generators,
|
||||||
|
|
||||||
...utils,
|
...utils,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,9 +24,13 @@
|
|||||||
"lib": "./lib"
|
"lib": "./lib"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash": "4.17.21",
|
"@strapi/strapi": "4.2.0",
|
||||||
|
"typescript": "4.6.2",
|
||||||
|
"chalk": "4.1.2",
|
||||||
|
"cli-table3": "0.6.2",
|
||||||
"fs-extra": "10.0.1",
|
"fs-extra": "10.0.1",
|
||||||
"typescript": "4.6.2"
|
"lodash": "4.17.21",
|
||||||
|
"prettier": "2.7.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.22.0 <=16.x.x",
|
"node": ">=12.22.0 <=16.x.x",
|
||||||
|
|||||||
@ -9150,7 +9150,7 @@ cli-table3@0.6.1:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
colors "1.4.0"
|
colors "1.4.0"
|
||||||
|
|
||||||
cli-table3@^0.6.1:
|
cli-table3@0.6.2, cli-table3@^0.6.1:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a"
|
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a"
|
||||||
integrity sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==
|
integrity sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==
|
||||||
@ -19341,6 +19341,11 @@ prettier@1.19.1:
|
|||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||||
|
|
||||||
|
prettier@2.7.1:
|
||||||
|
version "2.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
|
||||||
|
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
|
||||||
|
|
||||||
"prettier@>=2.2.1 <=2.3.0":
|
"prettier@>=2.2.1 <=2.3.0":
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user