@api-platform/client-generator
Version:
Generate apps built with Next, Nuxt, Quasar, React, React Native, Vue or Vuetify for any API documented using Hydra or OpenAPI
623 lines (562 loc) • 17.7 kB
JavaScript
import chalk from "chalk";
import BaseGenerator from "./BaseGenerator";
import handlebars from "handlebars";
import hbh_comparison from "handlebars-helpers/lib/comparison";
import hbh_array from "handlebars-helpers/lib/array";
import hbh_string from "handlebars-helpers/lib/string";
export default class extends BaseGenerator {
constructor(params) {
super(params);
this.registerTemplates(`quasar/`, [
// modules
"store/modules/foo/index.js",
"store/modules/foo/create/actions.js",
"store/modules/foo/create/getters.js",
"store/modules/foo/create/index.js",
"store/modules/foo/create/mutation_types.js",
"store/modules/foo/create/mutations.js",
"store/modules/foo/create/state.js",
"store/modules/foo/delete/actions.js",
"store/modules/foo/delete/getters.js",
"store/modules/foo/delete/index.js",
"store/modules/foo/delete/mutation_types.js",
"store/modules/foo/delete/mutations.js",
"store/modules/foo/delete/state.js",
"store/modules/foo/list/actions.js",
"store/modules/foo/list/getters.js",
"store/modules/foo/list/index.js",
"store/modules/foo/list/mutation_types.js",
"store/modules/foo/list/mutations.js",
"store/modules/foo/list/state.js",
"store/modules/foo/show/actions.js",
"store/modules/foo/show/getters.js",
"store/modules/foo/show/index.js",
"store/modules/foo/show/mutation_types.js",
"store/modules/foo/show/mutations.js",
"store/modules/foo/show/state.js",
"store/modules/foo/update/actions.js",
"store/modules/foo/update/getters.js",
"store/modules/foo/update/index.js",
"store/modules/foo/update/mutation_types.js",
"store/modules/foo/update/mutations.js",
"store/modules/foo/update/state.js",
// common for modules
"common/store/mutation_types.js",
"common/store/create/actions.js",
"common/store/create/getters.js",
"common/store/create/mutation_types.js",
"common/store/create/mutations.js",
"common/store/create/state.js",
"common/store/delete/actions.js",
"common/store/delete/getters.js",
"common/store/delete/mutation_types.js",
"common/store/delete/mutations.js",
"common/store/delete/state.js",
"common/store/list/actions.js",
"common/store/list/getters.js",
"common/store/list/mutation_types.js",
"common/store/list/mutations.js",
"common/store/list/state.js",
"common/store/show/actions.js",
"common/store/show/getters.js",
"common/store/show/mutation_types.js",
"common/store/show/mutations.js",
"common/store/show/state.js",
"common/store/update/actions.js",
"common/store/update/getters.js",
"common/store/update/mutation_types.js",
"common/store/update/mutations.js",
"common/store/update/state.js",
// components
"components/foo/Create.vue",
"components/foo/Filter.vue",
"components/foo/Form.vue",
"components/foo/List.vue",
"components/foo/Update.vue",
"components/foo/Show.vue",
//common components
"common/components/index.js",
"common/components/ActionCell.vue",
"common/components/Breadcrumb.vue",
"common/components/ConfirmDelete.vue",
"common/components/DataFilter.vue",
"common/components/InputDate.vue",
"common/components/Loading.vue",
"common/components/Toolbar.vue",
// components mixins
"common/mixins/CreateMixin.js",
"common/mixins/ListMixin.js",
"common/mixins/ShowMixin.js",
"common/mixins/UpdateMixin.js",
// routes
"router/foo.js",
// error
"error/SubmissionError.js",
// utils
"utils/fetch.js",
"utils/dates.js",
"utils/notify.js",
"utils/vuexer.js",
// i18n
"i18n/index.js",
]);
handlebars.registerHelper("compare", hbh_comparison.compare);
handlebars.registerHelper("ifEven", hbh_comparison.ifEven);
handlebars.registerHelper("ifOdd", hbh_comparison.ifOdd);
handlebars.registerHelper("inArray", hbh_array.inArray);
handlebars.registerHelper("forEach", hbh_array.forEach);
handlebars.registerHelper("downcase", hbh_string.downcase);
handlebars.registerHelper("capitalize", hbh_string.capitalize);
this.registerSwitchHelper();
}
help(resource) {
const titleLc = resource.title.toLowerCase();
console.log(
'Code for the "%s" resource type has been generated!',
resource.title
);
console.log(
"Paste the following definitions in your application configuration:"
);
console.log(
chalk.green(`
//Add to quasar.conf
// 1. in 'framework.components'
'QTable',
'QTh',
'QTr',
'QTd',
'QBreadcrumbs',
'QBreadcrumbsEl',
'QSpace',
'QInput',
'QForm',
'QSelect',
'QMarkupTable',
'QDate',
'QTime',
'QCheckbox',
'QPopupProxy',
'QSpinner',
'QInnerLoading',
'QCard',
'QCardSection',
'QCardActions',
'QExpansionItem',
'QDialog',
'QAvatar',
// 2. in 'framework.config', customize later
notify: {
position: 'top',
multiLine: true,
timeout: 0,
},
// 3. in 'framework.directives
'ClosePopup'
// 4. in 'framework.plugins
'Notify'
//import routes
import ${titleLc}Routes from '../router/${titleLc}';
// Add routes to VueRouter
const router = new VueRouter({
// ...
routes: [
...${titleLc}Routes,
]
});
// Add the modules in the store
import ${titleLc} from '../store/modules/${titleLc}/';
export const store = new Vuex.Store({
// ...
modules: {
${titleLc}
}
});
`)
);
}
registerSwitchHelper() {
/*
https://github.com/wycats/handlebars.js/issues/927#issuecomment-318640459
{{#switch state}}
{{#case "page1" "page2"}}page 1 or 2{{/case}}
{{#case "page3"}}page3{{/case}}
{{#case "page4"}}page4{{/case}}
{{#case "page5"}}
{{#switch s}}
{{#case "3"}}s = 3{{/case}}
{{#case "2"}}s = 2{{/case}}
{{#case "1"}}s = 1{{/case}}
{{#default}}unknown{{/default}}
{{/switch}}
{{/case}}
{{#default}}page0{{/default}}
{{/switch}}
*/
handlebars.__switch_stack__ = [];
handlebars.registerHelper("switch", function (value, options) {
handlebars.__switch_stack__.push({
switch_match: false,
switch_value: value,
});
let html = options.fn(this);
handlebars.__switch_stack__.pop();
return html;
});
handlebars.registerHelper("case", function (value, options) {
var args = Array.from(arguments);
options = args.pop();
var caseValues = args;
var stack =
handlebars.__switch_stack__[handlebars.__switch_stack__.length - 1];
if (stack.switch_match || caseValues.indexOf(stack.switch_value) === -1) {
return "";
} else {
stack.switch_match = true;
return options.fn(this);
}
});
handlebars.registerHelper("default", function (options) {
var stack =
handlebars.__switch_stack__[handlebars.__switch_stack__.length - 1];
if (!stack.switch_match) {
return options.fn(this);
}
});
}
generate(api, resource, dir) {
return resource
.getParameters()
.then((params) => {
params = params.map((param) => ({
...param,
...this.getHtmlInputTypeFromField(param),
}));
params = this.cleanupParams(params);
this.generateFiles(api, resource, dir, params);
})
.catch((e) => console.log(chalk.red(e)));
}
cleanupParams(params) {
const stats = {};
const result = [];
params.forEach((p) => {
let key = p.variable.endsWith("[]")
? p.variable.slice(0, -2)
: p.variable;
if (!stats[key]) {
stats[key] = 0;
}
stats[key] += 1;
});
params.forEach((p) => {
if (p.variable.startsWith("exists[")) {
result.push(p);
return; // removed for the moment, it can help to add null option to select
}
if (p.variable.startsWith("order[")) {
result.push(p);
return;
}
if (!stats[p.variable] && p.variable.endsWith("[]")) {
if (stats[p.variable.slice(0, -2)] === 1) {
result.push(p);
}
} else {
if (stats[p.variable] === 2) {
p.multiple = true;
}
result.push(p);
}
});
return result;
}
printObject(obj) {
var cache = [];
console.log(
JSON.stringify(obj, function (key, value) {
if (typeof value === "object" && value !== null) {
if (cache.includes(value)) {
// Duplicate reference found, discard key
return;
}
// Store value in our collection
cache.push(value);
}
return value;
})
);
cache = null;
}
generateFiles(api, resource, dir, params) {
const lc = resource.title.toLowerCase();
const titleUcFirst =
resource.title.charAt(0).toUpperCase() + resource.title.slice(1);
const formFields = this.buildFields(resource.writableFields);
const dateTypes = ["time", "date", "dateTime"];
const formContainsDate = formFields.some((e) => dateTypes.includes(e.type));
const fields = this.buildFields(resource.readableFields);
const listContainsDate = fields.some((e) => dateTypes.includes(e.type));
const parameters = [];
params.forEach((p) => {
const paramIndex = fields.findIndex((field) => field.name === p.variable);
if (paramIndex === -1) {
if (p.variable.startsWith("order[")) {
var v = p.variable.slice(6, -1);
var found = fields.findIndex((field) => field.name === v);
if (found !== -1) {
fields[found].sortable = true;
}
return;
}
if (p.variable.startsWith("exists[")) {
var exists = p.variable.slice(7, -1);
var foundExistsFieldIndex = fields.findIndex(
(field) => field.name === exists
);
if (foundExistsFieldIndex !== -1) {
const param = fields[foundExistsFieldIndex];
param.variable = p.variable;
param.filterType = "exists";
parameters.push(param);
} else {
p.name = exists;
p.filterType = "exists";
parameters.push(p);
}
return;
}
if (!p.name) {
p = { ...p, name: p.variable };
}
if (!p.sortable) {
parameters.push(p);
}
} else {
const param = fields[paramIndex];
param.multiple = p.multiple;
parameters.push(param);
}
});
const paramsHaveRefs = parameters.some(
(e) => e.type === "text" && e.reference
);
const labels = this.commonLabelTexts();
const hashEntry = this.hashCode(api.entrypoint);
const context = {
title: resource.title,
name: resource.name,
lc,
uc: resource.title.toUpperCase(),
fields,
dateTypes,
listContainsDate,
paramsHaveRefs,
parameters,
formFields,
formContainsDate,
hydraPrefix: this.hydraPrefix,
titleUcFirst,
labels,
hashEntry,
};
// Create directories
// These directories may already exist
for (let dir of [
`${dir}/config`,
`${dir}/error`,
`${dir}/router`,
`${dir}/utils`,
`${dir}/i18n`,
`${dir}/i18n/en-us`,
`${dir}/common`,
`${dir}/common/components`,
`${dir}/common/mixins`,
`${dir}/common/store`,
`${dir}/common/store/create`,
`${dir}/common/store/delete`,
`${dir}/common/store/list`,
`${dir}/common/store/show`,
`${dir}/common/store/update`,
]) {
this.createDir(dir, false);
}
for (let dir of [
`${dir}/store/modules/${lc}`,
`${dir}/store/modules/${lc}/create`,
`${dir}/store/modules/${lc}/delete`,
`${dir}/store/modules/${lc}/list`,
`${dir}/store/modules/${lc}/show`,
`${dir}/store/modules/${lc}/update`,
`${dir}/components/${lc}`,
]) {
this.createDir(dir);
}
for (let common of [
"common/components/index.js",
"common/components/ActionCell.vue",
"common/components/Breadcrumb.vue",
"common/components/ConfirmDelete.vue",
"common/components/DataFilter.vue",
"common/components/InputDate.vue",
"common/components/Loading.vue",
"common/components/Toolbar.vue",
"common/mixins/CreateMixin.js",
"common/mixins/ListMixin.js",
"common/mixins/ShowMixin.js",
"common/mixins/UpdateMixin.js",
"common/store/mutation_types.js",
"common/store/create/actions.js",
"common/store/create/getters.js",
"common/store/create/mutation_types.js",
"common/store/create/mutations.js",
"common/store/create/state.js",
"common/store/delete/actions.js",
"common/store/delete/getters.js",
"common/store/delete/mutation_types.js",
"common/store/delete/mutations.js",
"common/store/delete/state.js",
"common/store/list/actions.js",
"common/store/list/getters.js",
"common/store/list/mutation_types.js",
"common/store/list/mutations.js",
"common/store/list/state.js",
"common/store/show/actions.js",
"common/store/show/getters.js",
"common/store/show/mutation_types.js",
"common/store/show/mutations.js",
"common/store/show/state.js",
"common/store/update/actions.js",
"common/store/update/getters.js",
"common/store/update/mutation_types.js",
"common/store/update/mutations.js",
"common/store/update/state.js",
"utils/dates.js",
"utils/notify.js",
"utils/vuexer.js",
]) {
this.createFile(common, `${dir}/${common}`, context, false);
}
for (let pattern of [
// modules
"store/modules/%s/index.js",
"store/modules/%s/create/actions.js",
"store/modules/%s/create/getters.js",
"store/modules/%s/create/index.js",
"store/modules/%s/create/mutation_types.js",
"store/modules/%s/create/mutations.js",
"store/modules/%s/create/state.js",
"store/modules/%s/delete/actions.js",
"store/modules/%s/delete/getters.js",
"store/modules/%s/delete/index.js",
"store/modules/%s/delete/mutation_types.js",
"store/modules/%s/delete/mutations.js",
"store/modules/%s/delete/state.js",
"store/modules/%s/list/actions.js",
"store/modules/%s/list/getters.js",
"store/modules/%s/list/index.js",
"store/modules/%s/list/mutation_types.js",
"store/modules/%s/list/mutations.js",
"store/modules/%s/list/state.js",
"store/modules/%s/show/actions.js",
"store/modules/%s/show/getters.js",
"store/modules/%s/show/index.js",
"store/modules/%s/show/mutation_types.js",
"store/modules/%s/show/mutations.js",
"store/modules/%s/show/state.js",
"store/modules/%s/update/actions.js",
"store/modules/%s/update/getters.js",
"store/modules/%s/update/index.js",
"store/modules/%s/update/mutation_types.js",
"store/modules/%s/update/mutations.js",
"store/modules/%s/update/state.js",
// components
"components/%s/Create.vue",
"components/%s/Filter.vue",
"components/%s/Form.vue",
"components/%s/List.vue",
"components/%s/Update.vue",
"components/%s/Show.vue",
// routes
"router/%s.js",
]) {
if (
pattern === "components/%s/Filter.vue" &&
!context.parameters.length
) {
continue;
}
this.createFileFromPattern(pattern, dir, lc, context);
}
// error
this.createFile(
"error/SubmissionError.js",
`${dir}/error/SubmissionError.js`,
context,
false
);
this.createEntrypoint(
api.entrypoint,
`${dir}/config/${hashEntry}_entrypoint.js`
);
this.createFile(
"utils/fetch.js",
`${dir}/utils/fetch.js`,
{ hydraPrefix: this.hydraPrefix },
false
);
this.createFile(
"i18n/index.js",
`${dir}/i18n/en-us/index.js`,
{ labels: Object.values(labels) },
false
);
const contextLabels = {
labels: this.contextLabelTexts(formFields, fields),
};
this.createFile(
"i18n/index.js",
`${dir}/i18n/en-us/${lc}.js`,
contextLabels,
false
);
}
hashCode(s) {
return Math.abs(
Array.from(s).reduce(
(s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0,
0
)
);
}
contextLabelTexts(formFields, fields) {
let texts = [];
formFields.forEach((x) => texts.push(x.name)); // forms
fields.forEach((x) => texts.push(x.name)); // for show, too
return [...new Set(texts)];
}
commonLabelTexts() {
return {
submit: "Submit",
reset: "Reset",
delete: "Delete",
confirmDelete: "Are you sure you want to delete this item?",
noresults: "No results",
close: "Close",
cancel: "Cancel",
updated: "Updated",
field: "Field",
value: "Value",
filters: "Filters",
filter: "Filter",
unavail: "Data unavailable",
loading: "Loading...",
deleted: "Deleted",
numValidation: "Please, insert a value bigger than zero!",
stringValidation: "Please type something",
required: "Field is required",
recPerPage: "Records per page:",
};
// handlebars.registerPartial("myPartial", "{{name}}");
}
}