From c6999db6bc8ff3389eb7d0fec3eb73ade9d08c6b Mon Sep 17 00:00:00 2001 From: Valery Vargin Date: Sun, 15 Dec 2019 13:18:10 +0300 Subject: [PATCH 1/2] Templates for typescript react application --- src/generators.js | 4 +- src/generators/TypescriptGenerator.js | 217 ++++++++++++++++++ src/generators/TypescriptGenerator.test.js | 80 +++++++ .../TypescriptInterfaceGenerator.js | 94 -------- .../TypescriptInterfaceGenerator.test.js | 173 -------------- templates/typescript/actions/foo/create.ts | 55 +++++ templates/typescript/actions/foo/delete.ts | 39 ++++ templates/typescript/actions/foo/list.ts | 101 ++++++++ templates/typescript/actions/foo/show.ts | 92 ++++++++ templates/typescript/actions/foo/update.ts | 152 ++++++++++++ .../typescript/components/foo/Create.tsx | 69 ++++++ templates/typescript/components/foo/Form.tsx | 67 ++++++ templates/typescript/components/foo/List.tsx | 179 +++++++++++++++ templates/typescript/components/foo/Show.tsx | 143 ++++++++++++ .../typescript/components/foo/Update.tsx | 142 ++++++++++++ templates/typescript/components/foo/index.tsx | 6 + templates/typescript/interface.ts | 6 - templates/typescript/interfaces/Collection.ts | 20 ++ templates/typescript/interfaces/foo.ts | 8 + templates/typescript/reducers/foo/create.ts | 43 ++++ templates/typescript/reducers/foo/delete.ts | 43 ++++ templates/typescript/reducers/foo/index.ts | 8 + templates/typescript/reducers/foo/list.ts | 101 ++++++++ templates/typescript/reducers/foo/show.ts | 77 +++++++ templates/typescript/reducers/foo/update.ts | 130 +++++++++++ templates/typescript/routes/foo.tsx | 11 + templates/typescript/types/foo/create.ts | 26 +++ templates/typescript/types/foo/delete.ts | 26 +++ templates/typescript/types/foo/list.ts | 51 ++++ templates/typescript/types/foo/show.ts | 50 ++++ templates/typescript/types/foo/update.ts | 71 ++++++ templates/typescript/utils/dataAccess.ts | 90 ++++++++ templates/typescript/utils/types.ts | 10 + 33 files changed, 2109 insertions(+), 275 deletions(-) create mode 100644 src/generators/TypescriptGenerator.js create mode 100644 src/generators/TypescriptGenerator.test.js delete mode 100644 src/generators/TypescriptInterfaceGenerator.js delete mode 100644 src/generators/TypescriptInterfaceGenerator.test.js create mode 100644 templates/typescript/actions/foo/create.ts create mode 100644 templates/typescript/actions/foo/delete.ts create mode 100644 templates/typescript/actions/foo/list.ts create mode 100644 templates/typescript/actions/foo/show.ts create mode 100644 templates/typescript/actions/foo/update.ts create mode 100644 templates/typescript/components/foo/Create.tsx create mode 100644 templates/typescript/components/foo/Form.tsx create mode 100644 templates/typescript/components/foo/List.tsx create mode 100644 templates/typescript/components/foo/Show.tsx create mode 100644 templates/typescript/components/foo/Update.tsx create mode 100644 templates/typescript/components/foo/index.tsx delete mode 100644 templates/typescript/interface.ts create mode 100644 templates/typescript/interfaces/Collection.ts create mode 100644 templates/typescript/interfaces/foo.ts create mode 100644 templates/typescript/reducers/foo/create.ts create mode 100644 templates/typescript/reducers/foo/delete.ts create mode 100644 templates/typescript/reducers/foo/index.ts create mode 100644 templates/typescript/reducers/foo/list.ts create mode 100644 templates/typescript/reducers/foo/show.ts create mode 100644 templates/typescript/reducers/foo/update.ts create mode 100644 templates/typescript/routes/foo.tsx create mode 100644 templates/typescript/types/foo/create.ts create mode 100644 templates/typescript/types/foo/delete.ts create mode 100644 templates/typescript/types/foo/list.ts create mode 100644 templates/typescript/types/foo/show.ts create mode 100644 templates/typescript/types/foo/update.ts create mode 100644 templates/typescript/utils/dataAccess.ts create mode 100644 templates/typescript/utils/types.ts diff --git a/src/generators.js b/src/generators.js index 68e34c00..e2f15a99 100644 --- a/src/generators.js +++ b/src/generators.js @@ -2,7 +2,7 @@ import AdminOnRestGenerator from "./generators/AdminOnRestGenerator"; import NextGenerator from "./generators/NextGenerator"; import ReactGenerator from "./generators/ReactGenerator"; import ReactNativeGenerator from "./generators/ReactNativeGenerator"; -import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenerator"; +import TypescriptGenerator from "./generators/TypescriptGenerator"; import VueGenerator from "./generators/VueGenerator"; import VuetifyGenerator from "./generators/VuetifyGenerator"; import QuasarGenerator from "./generators/QuasarGenerator"; @@ -23,7 +23,7 @@ export default function generators(generator = "react") { case "react-native": return wrap(ReactNativeGenerator); case "typescript": - return wrap(TypescriptInterfaceGenerator); + return wrap(TypescriptGenerator); case "vue": return wrap(VueGenerator); case "vuetify": diff --git a/src/generators/TypescriptGenerator.js b/src/generators/TypescriptGenerator.js new file mode 100644 index 00000000..cf35e539 --- /dev/null +++ b/src/generators/TypescriptGenerator.js @@ -0,0 +1,217 @@ +import chalk from "chalk"; +import BaseGenerator from "./BaseGenerator"; + +export default class TypescriptGenerator extends BaseGenerator { + constructor(params) { + super(params); + + this.registerTemplates("typescript/", [ + // actions + "actions/foo/create.ts", + "actions/foo/delete.ts", + "actions/foo/list.ts", + "actions/foo/update.ts", + "actions/foo/show.ts", + + // utils + "utils/dataAccess.ts", + "utils/types.ts", + + // reducers + "reducers/foo/create.ts", + "reducers/foo/delete.ts", + "reducers/foo/index.ts", + "reducers/foo/list.ts", + "reducers/foo/update.ts", + "reducers/foo/show.ts", + + // types + "types/foo/create.ts", + "types/foo/delete.ts", + "types/foo/list.ts", + "types/foo/show.ts", + "types/foo/update.ts", + + // interfaces + "interfaces/Collection.ts", + "interfaces/foo.ts", + + // components + "components/foo/Create.tsx", + "components/foo/Form.tsx", + "components/foo/index.tsx", + "components/foo/List.tsx", + "components/foo/Update.tsx", + "components/foo/Show.tsx", + + // routes + "routes/foo.tsx" + ]); + } + + 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 (`client/src/index.tsx` by default):" + ); + console.log( + chalk.green(` +// import reducers +import ${titleLc} from './reducers/${titleLc}/'; + +//import routes +import ${titleLc}Routes from './routes/${titleLc}'; + +// Add the reducer +combineReducers({ ${titleLc},/* ... */ }), + +// Add routes to +{ ${titleLc}Routes } +`) + ); + } + + generate(api, resource, dir) { + const lc = resource.title.toLowerCase(); + const titleUcFirst = this.ucFirst(resource.title); + const { fields, imports } = this.parseFields(resource); + + const context = { + name: resource.name, + lc, + uc: resource.title.toUpperCase(), + ucf: titleUcFirst, + titleUcFirst, + fields, + formFields: this.buildFields(fields), + imports, + hydraPrefix: this.hydraPrefix, + title: resource.title + }; + + // Create directories + // These directories may already exist + [ + `${dir}/utils`, + `${dir}/config`, + `${dir}/interfaces`, + `${dir}/routes`, + `${dir}/actions/${lc}`, + `${dir}/types/${lc}`, + `${dir}/components/${lc}`, + `${dir}/reducers/${lc}` + ].forEach(dir => this.createDir(dir)); + + [ + // actions + "actions/%s/create.ts", + "actions/%s/delete.ts", + "actions/%s/list.ts", + "actions/%s/update.ts", + "actions/%s/show.ts", + + // components + "components/%s/Create.tsx", + "components/%s/Form.tsx", + "components/%s/index.tsx", + "components/%s/List.tsx", + "components/%s/Update.tsx", + "components/%s/Show.tsx", + + // reducers + "reducers/%s/create.ts", + "reducers/%s/delete.ts", + "reducers/%s/index.ts", + "reducers/%s/list.ts", + "reducers/%s/update.ts", + "reducers/%s/show.ts", + + // types + "types/%s/create.ts", + "types/%s/delete.ts", + "types/%s/list.ts", + "types/%s/show.ts", + "types/%s/update.ts", + + // routes + "routes/%s.tsx" + ].forEach(pattern => this.createFileFromPattern(pattern, dir, lc, context)); + + // interface pattern should be camel cased + this.createFile( + "interfaces/foo.ts", + `${dir}/interfaces/${context.ucf}.ts`, + context + ); + + // copy with regular name + [ + // interfaces + "interfaces/Collection.ts", + + // utils + "utils/dataAccess.ts", + "utils/types.ts" + ].forEach(file => this.createFile(file, `${dir}/${file}`, context, false)); + + // API config + this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.ts`); + } + + getDescription(field) { + return field.description ? field.description.replace(/"/g, "'") : ""; + } + + parseFields(resource) { + const fields = [ + ...resource.writableFields, + ...resource.readableFields + ].reduce((list, field) => { + if (list[field.name]) { + return list; + } + + return { + ...list, + [field.name]: { + notrequired: !field.required, + name: field.name, + type: this.getType(field), + description: this.getDescription(field), + readonly: false, + reference: field.reference + } + }; + }, {}); + + // Parse fields to add relevant imports, required for Typescript + const fieldsArray = Object.values(fields); + const imports = Object.values(fields).reduce( + (list, { reference, type }) => { + if (!reference) { + return list; + } + + return { + ...list, + [type]: { + type, + file: `./${type}` + } + }; + }, + {} + ); + + return { fields: fieldsArray, imports: Object.values(imports) }; + } + + ucFirst(target) { + return target.charAt(0).toUpperCase() + target.slice(1); + } +} diff --git a/src/generators/TypescriptGenerator.test.js b/src/generators/TypescriptGenerator.test.js new file mode 100644 index 00000000..98275a58 --- /dev/null +++ b/src/generators/TypescriptGenerator.test.js @@ -0,0 +1,80 @@ +import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib"; +import fs from "fs"; +import tmp from "tmp"; +import TypescriptGenerator from "./TypescriptGenerator"; + +test("Generate a Typescript React app", () => { + const generator = new TypescriptGenerator({ + hydraPrefix: "hydra:", + templateDirectory: `${__dirname}/../../templates` + }); + const tmpobj = tmp.dirSync({ unsafeCleanup: true }); + + const fields = [ + new Field("bar", { + id: "http://schema.org/url", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: true, + description: "An URL" + }) + ]; + const resource = new Resource("abc", "http://example.com/foos", { + id: "abc", + title: "abc", + readableFields: fields, + writableFields: fields + }); + const api = new Api("http://example.com", { + entrypoint: "http://example.com:8080", + title: "My API", + resources: [resource] + }); + generator.generate(api, resource, tmpobj.name); + + [ + "/utils/dataAccess.ts", + "/utils/types.ts", + "/config/entrypoint.ts", + + "/interfaces/Abc.ts", + "/interfaces/Collection.ts", + + "/actions/abc/create.ts", + "/actions/abc/delete.ts", + "/actions/abc/list.ts", + "/actions/abc/show.ts", + "/actions/abc/update.ts", + + "/types/abc/create.ts", + "/types/abc/delete.ts", + "/types/abc/list.ts", + "/types/abc/show.ts", + "/types/abc/update.ts", + + "/components/abc/index.tsx", + "/components/abc/Create.tsx", + "/components/abc/Update.tsx", + + "/routes/abc.tsx", + + "/reducers/abc/create.ts", + "/reducers/abc/delete.ts", + "/reducers/abc/index.ts", + "/reducers/abc/list.ts", + "/reducers/abc/show.ts", + "/reducers/abc/update.ts" + ].forEach(file => expect(fs.existsSync(tmpobj.name + file)).toBe(true)); + + [ + "/components/abc/Form.tsx", + "/components/abc/List.tsx", + "/components/abc/Show.tsx", + "/interfaces/Abc.ts" + ].forEach(file => { + expect(fs.existsSync(tmpobj.name + file)).toBe(true); + expect(fs.readFileSync(tmpobj.name + file, "utf8")).toMatch(/bar/); + }); + + tmpobj.removeCallback(); +}); diff --git a/src/generators/TypescriptInterfaceGenerator.js b/src/generators/TypescriptInterfaceGenerator.js deleted file mode 100644 index d8f4e37a..00000000 --- a/src/generators/TypescriptInterfaceGenerator.js +++ /dev/null @@ -1,94 +0,0 @@ -import BaseGenerator from "./BaseGenerator"; - -export default class TypescriptInterfaceGenerator extends BaseGenerator { - constructor(params) { - super(params); - - this.registerTemplates(`typescript/`, ["interface.ts"]); - } - - help(resource) { - console.log( - 'Interface for the "%s" resource type has been generated!', - resource.title - ); - } - - generate(api, resource, dir) { - const dest = `${dir}/interfaces`; - const { fields, imports } = this.parseFields(resource); - - this.createDir(dest, false); - this.createFile( - "interface.ts", - `${dest}/${resource.title.toLowerCase()}.ts`, - { - fields, - imports, - name: resource.title - } - ); - } - - getDescription(field) { - return field.description ? field.description.replace(/"/g, "'") : ""; - } - - parseFields(resource) { - const fields = {}; - - for (let field of resource.writableFields) { - fields[field.name] = { - notrequired: !field.required, - name: field.name, - type: this.getType(field), - description: this.getDescription(field), - readonly: false, - reference: field.reference - }; - } - - for (let field of resource.readableFields) { - if (fields[field.name] !== undefined) { - continue; - } - - fields[field.name] = { - notrequired: !field.required, - name: field.name, - type: this.getType(field), - description: this.getDescription(field), - readonly: true, - reference: field.reference - }; - } - - // If id is not present, add it manually with default values - if (!("id" in fields)) { - fields["id"] = { - notrequired: true, - name: "id", - type: "string", - description: null, - readonly: false - }; - } - - // Parse fields to add relevant imports, required for Typescript - const fieldsArray = Object.keys(fields).map(e => fields[e]); - const imports = {}; - - for (const field of fieldsArray) { - if (field.reference) { - imports[field.type] = { - type: field.type, - file: "./" + field.type.toLowerCase() - }; - } - } - - const importsArray = Object.keys(imports).map(e => imports[e]); - - return { fields: fieldsArray, imports: importsArray }; - } -} diff --git a/src/generators/TypescriptInterfaceGenerator.test.js b/src/generators/TypescriptInterfaceGenerator.test.js deleted file mode 100644 index a60375ab..00000000 --- a/src/generators/TypescriptInterfaceGenerator.test.js +++ /dev/null @@ -1,173 +0,0 @@ -import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib"; -import fs from "fs"; -import tmp from "tmp"; -import TypescriptInterfaceGenerator from "./TypescriptInterfaceGenerator"; - -test("Generate a typescript interface", () => { - const generator = new TypescriptInterfaceGenerator({ - templateDirectory: `${__dirname}/../../templates` - }); - const tmpobj = tmp.dirSync({ unsafeCleanup: true }); - - const resource = new Resource("abc", "http://example.com/foos", { - id: "foo", - title: "Foo", - readableFields: [ - new Field("bar", { - id: "http://schema.org/url", - range: "http://www.w3.org/2001/XMLSchema#string", - reference: null, - required: true, - description: "An URL" - }) - ], - writableFields: [ - new Field("foo", { - id: "http://schema.org/url", - range: "http://www.w3.org/2001/XMLSchema#datetime", - reference: null, - required: true, - description: "An URL" - }), - new Field("foobar", { - id: "http://schema.org/url", - range: undefined, - reference: new Resource("foobar", "http://example.com/FooBar", { - title: "FooBar" - }), - required: false - }) - ] - }); - const api = new Api("http://example.com", { - entrypoint: "http://example.com:8080", - title: "My API", - resources: [resource] - }); - generator.generate(api, resource, tmpobj.name); - - expect(fs.existsSync(tmpobj.name + "/interfaces/foo.ts")).toBe(true); - - const res = `export interface Foo { - '@id'?: string; - foo: any; - foobar?: string[]; - readonly bar: string; - id?: string; -} -`; - expect( - fs.readFileSync(tmpobj.name + "/interfaces/foo.ts").toString() - ).toEqual(res); - - tmpobj.removeCallback(); -}); - -test("Generate a typescript interface without references to other interfaces", () => { - const generator = new TypescriptInterfaceGenerator({ - templateDirectory: `${__dirname}/../../templates` - }); - const tmpobj = tmp.dirSync({ unsafeCleanup: true }); - - const resource = new Resource("abc", "http://example.com/foos", { - id: "foo", - title: "Foo", - readableFields: [ - new Field("bar", { - id: "http://schema.org/url", - range: "http://www.w3.org/2001/XMLSchema#string", - reference: null, - required: true, - description: "An URL" - }) - ], - writableFields: [ - new Field("foo", { - id: "http://schema.org/url", - range: "http://www.w3.org/2001/XMLSchema#datetime", - reference: null, - required: true, - description: "An URL" - }) - ] - }); - const api = new Api("http://example.com", { - entrypoint: "http://example.com:8080", - title: "My API", - resources: [resource] - }); - generator.generate(api, resource, tmpobj.name); - - expect(fs.existsSync(tmpobj.name + "/interfaces/foo.ts")).toBe(true); - - const res = `export interface Foo { - '@id'?: string; - foo: any; - readonly bar: string; - id?: string; -} -`; - expect( - fs.readFileSync(tmpobj.name + "/interfaces/foo.ts").toString() - ).toEqual(res); - - tmpobj.removeCallback(); -}); - -test("Generate a typescript interface with an explicit id field in the readableFields", () => { - const generator = new TypescriptInterfaceGenerator({ - templateDirectory: `${__dirname}/../../templates` - }); - const tmpobj = tmp.dirSync({ unsafeCleanup: true }); - - const resource = new Resource("abc", "http://example.com/foos", { - id: "foo", - title: "Foo", - readableFields: [ - new Field("bar", { - id: "http://schema.org/url", - range: "http://www.w3.org/2001/XMLSchema#string", - reference: null, - required: true, - description: "An URL" - }), - new Field("id", { - id: "http://schema.org/url", - range: "http://www.w3.org/2001/XMLSchema#string", - reference: null, - required: false, - description: "Id" - }) - ], - writableFields: [ - new Field("foo", { - id: "http://schema.org/url", - range: "http://www.w3.org/2001/XMLSchema#datetime", - reference: null, - required: true, - description: "An URL" - }) - ] - }); - const api = new Api("http://example.com", { - entrypoint: "http://example.com:8080", - title: "My API", - resources: [resource] - }); - generator.generate(api, resource, tmpobj.name); - - expect(fs.existsSync(tmpobj.name + "/interfaces/foo.ts")).toBe(true); - - const res = `export interface Foo { - '@id'?: string; - foo: any; - readonly bar: string; - readonly id?: string; -} -`; - expect( - fs.readFileSync(tmpobj.name + "/interfaces/foo.ts").toString() - ).toEqual(res); - - tmpobj.removeCallback(); -}); diff --git a/templates/typescript/actions/foo/create.ts b/templates/typescript/actions/foo/create.ts new file mode 100644 index 00000000..5dfd1c98 --- /dev/null +++ b/templates/typescript/actions/foo/create.ts @@ -0,0 +1,55 @@ +import { SubmissionError } from 'redux-form'; +import { fetchApi } from '../../utils/dataAccess'; +import { TError, TDispatch } from '../../utils/types'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { + {{{uc}}}_CREATE_ERROR, + {{{uc}}}_CREATE_LOADING, + {{{uc}}}_CREATE_SUCCESS, + IActionError, + IActionLoading, + IActionSuccess +} from '../../types/{{{lc}}}/create'; + +export function error(error: TError): IActionError { + return { type: {{{uc}}}_CREATE_ERROR, error }; +} + +export function loading(loading: boolean): IActionLoading { + return { type: {{{uc}}}_CREATE_LOADING, loading }; +} + +export function success(created: I{{{ucf}}} | null): IActionSuccess { + return { type: {{{uc}}}_CREATE_SUCCESS, created }; +} + +export function create(values: Partial) { + return (dispatch: TDispatch) => { + dispatch(loading(true)); + + return fetchApi('{{{name}}}', { method: 'POST', body: JSON.stringify(values) }) + .then(response => { + dispatch(loading(false)); + + return response.json(); + }) + .then(retrieved => dispatch(success(retrieved))) + .catch(e => { + dispatch(loading(false)); + + if (e instanceof SubmissionError) { + dispatch(error(e.errors._error)); + throw e; + } + + dispatch(error(e.message)); + }); + }; +} + +export function reset() { + return (dispatch: TDispatch) => { + dispatch(loading(false)); + dispatch(error(null)); + }; +} diff --git a/templates/typescript/actions/foo/delete.ts b/templates/typescript/actions/foo/delete.ts new file mode 100644 index 00000000..1a04a8f9 --- /dev/null +++ b/templates/typescript/actions/foo/delete.ts @@ -0,0 +1,39 @@ +import { fetchApi } from '../../utils/dataAccess'; +import { TError, TDispatch, IResource } from '../../utils/types'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { + {{{uc}}}_DELETE_ERROR, + {{{uc}}}_DELETE_LOADING, + {{{uc}}}_DELETE_SUCCESS, + IActionError, + IActionLoading, + IActionSuccess +} from '../../types/{{{lc}}}/delete'; + +export function error(error: TError): IActionError { + return { type: {{{uc}}}_DELETE_ERROR, error }; +} + +export function loading(loading: boolean): IActionLoading { + return { type: {{{uc}}}_DELETE_LOADING, loading }; +} + +export function success(deleted: Partial & IResource | null): IActionSuccess { + return { type: {{{uc}}}_DELETE_SUCCESS, deleted }; +} + +export function del(item: Partial & IResource) { + return (dispatch: TDispatch) => { + dispatch(loading(true)); + + return fetchApi(item['@id'], { method: 'DELETE' }) + .then(() => { + dispatch(loading(false)); + dispatch(success(item)); + }) + .catch(e => { + dispatch(loading(false)); + dispatch(error(e.message)); + }); + }; +} diff --git a/templates/typescript/actions/foo/list.ts b/templates/typescript/actions/foo/list.ts new file mode 100644 index 00000000..f8ca70cd --- /dev/null +++ b/templates/typescript/actions/foo/list.ts @@ -0,0 +1,101 @@ +import { + fetchApi, + normalize, + extractHubURL, + mercureSubscribe as subscribe +} from '../../utils/dataAccess'; +import { success as deleteSuccess } from './delete'; +import { TError, TDispatch, IResource } from '../../utils/types' +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { IPagedCollection } from '../../interfaces/Collection' +import { + {{{uc}}}_LIST_ERROR, + {{{uc}}}_LIST_LOADING, + {{{uc}}}_LIST_MERCURE_DELETED, + {{{uc}}}_LIST_MERCURE_MESSAGE, + {{{uc}}}_LIST_MERCURE_OPEN, + {{{uc}}}_LIST_RESET, + {{{uc}}}_LIST_SUCCESS, + IActionError, + IActionLoading, + IActionSuccess, + IActionMercureOpen +} from '../../types/{{{lc}}}/list' + +export function error(error: TError): IActionError { + return { type: {{{uc}}}_LIST_ERROR, error }; +} + +export function loading(loading: boolean): IActionLoading { + return { type: {{{uc}}}_LIST_LOADING, loading }; +} + +export function success(retrieved: IPagedCollection): IActionSuccess { + return { type: {{{uc}}}_LIST_SUCCESS, retrieved }; +} + +export function list(page = '{{{name}}}') { + return (dispatch: TDispatch) => { + dispatch(loading(true)); + dispatch(error('')); + + fetchApi(page) + .then(response => + response + .json() + .then(retrieved => ({ retrieved, hubURL: extractHubURL(response) })) + ) + .then(({ retrieved, hubURL }) => { + retrieved = normalize(retrieved); + + dispatch(loading(false)); + dispatch(success(retrieved)); + + if (hubURL && retrieved['hydra:member'].length) + dispatch( + mercureSubscribe( + hubURL, + retrieved['hydra:member'].map((i: I{{{ucf}}}) => i['@id']) + ) + ); + }) + .catch(e => { + dispatch(loading(false)); + dispatch(error(e.message)); + }); + }; +} + +export function reset(eventSource: EventSource | null) { + return (dispatch: TDispatch) => { + if (eventSource) eventSource.close(); + + dispatch({ type: {{{uc}}}_LIST_RESET }); + dispatch(deleteSuccess(null)); + }; +} + +export function mercureSubscribe(hubURL: URL, topics: string[]) { + return (dispatch: TDispatch) => { + const eventSource = subscribe(hubURL, topics); + dispatch(mercureOpen(eventSource)); + eventSource.addEventListener('message', event => + dispatch(mercureMessage(normalize(JSON.parse(event.data)))) + ); + }; +} + +export function mercureOpen(eventSource: EventSource): IActionMercureOpen { + return { type: {{{uc}}}_LIST_MERCURE_OPEN, eventSource }; +} + +export function mercureMessage(retrieved: IResource | I{{{ucf}}}) { + return (dispatch: TDispatch) => { + if (1 === Object.keys(retrieved).length) { + dispatch({ type: {{{uc}}}_LIST_MERCURE_DELETED, retrieved }); + return; + } + + dispatch({ type: {{{uc}}}_LIST_MERCURE_MESSAGE, retrieved }); + }; +} diff --git a/templates/typescript/actions/foo/show.ts b/templates/typescript/actions/foo/show.ts new file mode 100644 index 00000000..4d4e3a6a --- /dev/null +++ b/templates/typescript/actions/foo/show.ts @@ -0,0 +1,92 @@ +import { + fetchApi, + extractHubURL, + normalize, + mercureSubscribe as subscribe +} from '../../utils/dataAccess'; +import { TError, TDispatch, IResource } from '../../utils/types'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { + {{{uc}}}_SHOW_ERROR, + {{{uc}}}_SHOW_LOADING, + {{{uc}}}_SHOW_SUCCESS, + {{{uc}}}_SHOW_RESET, + {{{uc}}}_SHOW_MERCURE_OPEN, + {{{uc}}}_SHOW_MERCURE_DELETED, + {{{uc}}}_SHOW_MERCURE_MESSAGE, + IActionError, + IActionLoading, + IActionSuccess +} from '../../types/{{{lc}}}/show'; + +export function error(error: TError): IActionError { + return { type: {{{uc}}}_SHOW_ERROR, error }; +} + +export function loading(loading: boolean): IActionLoading { + return { type: {{{uc}}}_SHOW_LOADING, loading }; +} + +export function success(retrieved: I{{{ucf}}}): IActionSuccess { + return { type: {{{uc}}}_SHOW_SUCCESS, retrieved }; +} + +export function retrieve(id: string) { + return (dispatch: TDispatch) => { + dispatch(loading(true)); + + return fetchApi(id) + .then(response => + response + .json() + .then(retrieved => ({ retrieved, hubURL: extractHubURL(response) })) + ) + .then(({ retrieved, hubURL }) => { + retrieved = normalize(retrieved); + + dispatch(loading(false)); + dispatch(success(retrieved)); + + if (hubURL) dispatch(mercureSubscribe(hubURL, retrieved['@id'])); + }) + .catch(e => { + dispatch(loading(false)); + dispatch(error(e.message)); + }); + }; +} + +export function reset(eventSource: EventSource | null) { + return (dispatch: TDispatch) => { + if (eventSource) eventSource.close(); + + dispatch({ type: {{{uc}}}_SHOW_RESET }); + dispatch(error(null)); + dispatch(loading(false)); + }; +} + +export function mercureSubscribe(hubURL: URL, topic: string) { + return (dispatch: TDispatch) => { + const eventSource = subscribe(hubURL, [topic]); + dispatch(mercureOpen(eventSource)); + eventSource.addEventListener('message', event => + dispatch(mercureMessage(normalize(JSON.parse(event.data)))) + ); + }; +} + +export function mercureOpen(eventSource: EventSource) { + return { type: {{{uc}}}_SHOW_MERCURE_OPEN, eventSource }; +} + +export function mercureMessage(retrieved: IResource | I{{{ucf}}}) { + return (dispatch: TDispatch) => { + if (1 === Object.keys(retrieved).length) { + dispatch({ type: {{{uc}}}_SHOW_MERCURE_DELETED, retrieved }); + return; + } + + dispatch({ type: {{{uc}}}_SHOW_MERCURE_MESSAGE, retrieved }); + }; +} diff --git a/templates/typescript/actions/foo/update.ts b/templates/typescript/actions/foo/update.ts new file mode 100644 index 00000000..2ba07d76 --- /dev/null +++ b/templates/typescript/actions/foo/update.ts @@ -0,0 +1,152 @@ +import { SubmissionError } from 'redux-form'; +import { + fetchApi, + extractHubURL, + normalize, + mercureSubscribe as subscribe +} from '../../utils/dataAccess'; +import { success as createSuccess } from './create'; +import { loading, error } from './delete'; +import { TError, TDispatch, IResource } from '../../utils/types'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { + {{{uc}}}_UPDATE_RETRIEVE_ERROR, + {{{uc}}}_UPDATE_RETRIEVE_LOADING, + {{{uc}}}_UPDATE_RETRIEVE_SUCCESS, + {{{uc}}}_UPDATE_UPDATE_ERROR, + {{{uc}}}_UPDATE_UPDATE_LOADING, + {{{uc}}}_UPDATE_UPDATE_SUCCESS, + {{{uc}}}_UPDATE_RESET, + {{{uc}}}_UPDATE_MERCURE_OPEN, + {{{uc}}}_UPDATE_MERCURE_DELETED, + {{{uc}}}_UPDATE_MERCURE_MESSAGE, + IActionRetrieveError, + IActionRetrieveLoading, + IActionRetrieveSuccess, + IActionUpdateError, + IActionUpdateLoading, + IActionUpdateSuccess, + IActionMercureOpen +} from '../../types/{{{lc}}}/update' + +export function retrieveError(retrieveError: TError): IActionRetrieveError { + return { type: {{{uc}}}_UPDATE_RETRIEVE_ERROR, retrieveError }; +} + +export function retrieveLoading(retrieveLoading: boolean): IActionRetrieveLoading { + return { type: {{{uc}}}_UPDATE_RETRIEVE_LOADING, retrieveLoading }; +} + +export function retrieveSuccess(retrieved: I{{{ucf}}}): IActionRetrieveSuccess { + return { type: {{{uc}}}_UPDATE_RETRIEVE_SUCCESS, retrieved }; +} + +export function retrieve(id: string) { + return (dispatch: TDispatch) => { + dispatch(retrieveLoading(true)); + + return fetchApi(id) + .then(response => + response + .json() + .then(retrieved => ({ retrieved, hubURL: extractHubURL(response) })) + ) + .then(({ retrieved, hubURL }) => { + retrieved = normalize(retrieved); + + dispatch(retrieveLoading(false)); + dispatch(retrieveSuccess(retrieved)); + + if (hubURL) dispatch(mercureSubscribe(hubURL, retrieved['@id'])); + }) + .catch(e => { + dispatch(retrieveLoading(false)); + dispatch(retrieveError(e.message)); + }); + }; +} + +export function updateError(updateError: TError): IActionUpdateError { + return { type: {{{uc}}}_UPDATE_UPDATE_ERROR, updateError }; +} + +export function updateLoading(updateLoading: boolean): IActionUpdateLoading { + return { type: {{{uc}}}_UPDATE_UPDATE_LOADING, updateLoading }; +} + +export function updateSuccess(updated: I{{{ucf}}}): IActionUpdateSuccess { + return { type: {{{uc}}}_UPDATE_UPDATE_SUCCESS, updated }; +} + +export function update(item: IResource, values: Partial) { + return (dispatch: TDispatch) => { + dispatch(updateError(null)); + dispatch(createSuccess(null)); + dispatch(updateLoading(true)); + + return fetchApi(item['@id'], { + method: 'PUT', + headers: new Headers({ 'Content-Type': 'application/ld+json' }), + body: JSON.stringify(values) + }) + .then(response => + response + .json() + .then(retrieved => ({ retrieved, hubURL: extractHubURL(response) })) + ) + .then(({ retrieved, hubURL }) => { + retrieved = normalize(retrieved); + + dispatch(updateLoading(false)); + dispatch(updateSuccess(retrieved)); + + if (hubURL) dispatch(mercureSubscribe(hubURL, retrieved['@id'])); + }) + .catch(e => { + dispatch(updateLoading(false)); + + if (e instanceof SubmissionError) { + dispatch(updateError(e.errors._error)); + throw e; + } + + dispatch(updateError(e.message)); + }); + }; +} + +export function reset(eventSource: EventSource | null) { + return (dispatch: TDispatch) => { + if (eventSource) eventSource.close(); + + dispatch({ type: {{{uc}}}_UPDATE_RESET }); + dispatch(error(null)); + dispatch(loading(false)); + dispatch(createSuccess(null)); + }; +} + +export function mercureSubscribe(hubURL: URL, topic: string) { + return (dispatch: TDispatch) => { + const eventSource = subscribe(hubURL, [topic]); + dispatch(mercureOpen(eventSource)); + eventSource.addEventListener('message', event => + dispatch(mercureMessage(normalize(JSON.parse(event.data)))) + ); + }; +} + +export function mercureOpen(eventSource: EventSource): IActionMercureOpen { + return { type: {{{uc}}}_UPDATE_MERCURE_OPEN, eventSource }; +} + +export function mercureMessage(retrieved: IResource | I{{{ucf}}}) { + return (dispatch: TDispatch) => { + if (1 === Object.keys(retrieved).length) { + dispatch({ type: {{{uc}}}_UPDATE_MERCURE_DELETED, retrieved }); + return; + } + + dispatch({ type: {{{uc}}}_UPDATE_MERCURE_MESSAGE, retrieved }); + }; +} diff --git a/templates/typescript/components/foo/Create.tsx b/templates/typescript/components/foo/Create.tsx new file mode 100644 index 00000000..35488702 --- /dev/null +++ b/templates/typescript/components/foo/Create.tsx @@ -0,0 +1,69 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Link, Redirect } from 'react-router-dom'; +import { TError, TDispatch } from '../../utils/types'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { create, reset } from '../../actions/{{{lc}}}/create'; +import { ICreateState } from '../../types/{{{lc}}}/create'; +import Form from './Form'; + +interface ICreateProps { + created: I{{{ucf}}} | null; + loading: boolean; + error: TError; +} + +interface ICreateActions { + reset: () => any; + create: ({{{lc}}}: Partial) => any; +} + +const mapStateToProps: (state: { {{{lc}}}: { create: ICreateState } }) => ICreateProps = state => { + const { created, error, loading } = state.{{{lc}}}.create; + return { created, error, loading }; +}; + +const mapDispatchToProps: (dispatch: TDispatch) => ICreateActions = dispatch => ({ + create: values => dispatch(create(values)), + reset: () => dispatch(reset()) +}); + +class Create extends Component { + componentWillUnmount() { + this.props.reset(); + } + + render() { + if (this.props.created) + return ( + + ); + + return ( +
+

New {{{title}}}

+ + {this.props.loading && ( +
+ Loading... +
+ )} + {this.props.error && ( +
+
+ )} + +
+ + Back to list + +
+ ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Create); diff --git a/templates/typescript/components/foo/Form.tsx b/templates/typescript/components/foo/Form.tsx new file mode 100644 index 00000000..141f9a9e --- /dev/null +++ b/templates/typescript/components/foo/Form.tsx @@ -0,0 +1,67 @@ +import React, { Component } from 'react'; +import { Field, InjectedFormProps, reduxForm } from 'redux-form'; + +class Form extends Component { + renderField: React.FC = data => { + data.input.className = 'form-control'; + + const isInvalid = data.meta.touched && !!data.meta.error; + if (isInvalid) { + data.input.className += ' is-invalid'; + data.input['aria-invalid'] = true; + } + + if (this.props.error && data.meta.touched && !data.meta.error) { + data.input.className += ' is-valid'; + } + + return ( +
+ + + {isInvalid &&
{data.meta.error}
} +
+ ); + }; + + render() { + return ( + +{{#each formFields}} + (v === '' ? [] : v.split(','))}{{/unless}}{{/if}}{{#if number}} + normalize={(v: string) => parseFloat(v)}{{/if}} + /> +{{/each}} + + + + ); + } +} + +export default reduxForm({ + form: '{{{lc}}}', + enableReinitialize: true, + keepDirtyOnReinitialize: true +})(Form); diff --git a/templates/typescript/components/foo/List.tsx b/templates/typescript/components/foo/List.tsx new file mode 100644 index 00000000..e1784dab --- /dev/null +++ b/templates/typescript/components/foo/List.tsx @@ -0,0 +1,179 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { RouteComponentProps } from 'react-router'; +import { Link } from 'react-router-dom'; +import { TError, TDispatch } from '../../utils/types'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { IPagedCollection } from '../../interfaces/Collection'; +import { list, reset } from '../../actions/{{{lc}}}/list'; +import { IListState } from '../../types/{{{lc}}}/list'; + +interface IListProps { + retrieved: IPagedCollection | null; + loading: boolean; + error: TError; + eventSource: EventSource | null; +} + +interface IListActions { + list: (page?: string) => any; + reset: (eventSource: EventSource | null) => any; +} + +const mapStateToProps: (state: { {{{lc}}}: { list: IListState } }) => IListProps = state => { + const { + retrieved, + loading, + error, + eventSource + } = state.{{{lc}}}.list; + return { retrieved, loading, error, eventSource }; +}; + +const mapDispatchToProps: (dispatch: TDispatch) => IListActions = dispatch => ({ + list: page => dispatch(list(page)), + reset: eventSource => dispatch(reset(eventSource)) +}); + +type TListProps = RouteComponentProps & IListProps & IListActions; + +class List extends Component { + componentDidMount() { + this.props.list( + this.props.match.params.page && + decodeURIComponent(this.props.match.params.page) + ); + } + + componentWillReceiveProps(nextProps: TListProps) { + if (this.props.match.params.page !== nextProps.match.params.page) + nextProps.list( + nextProps.match.params.page && + decodeURIComponent(nextProps.match.params.page) + ); + } + + componentWillUnmount() { + this.props.reset(this.props.eventSource); + } + + render() { + return ( +
+

{{{title}}} List

+ + {this.props.loading && ( +
Loading...
+ )} + {this.props.error && ( +
{this.props.error}
+ )} + +

+ + Create + +

+ + + + + +{{#each fields}} + +{{/each}} + + + + {this.props.retrieved && this.props.retrieved['hydra:member'] && + this.props.retrieved['hydra:member'].map(item => ( + + +{{#each fields}} + +{{/each}} + + + + ))} + +
id{{name}} +
+ + {item['@id']} + + {{#if reference}}{this.renderLinks('{{{reference.name}}}', item['{{{name}}}'])}{{else}}{item['{{{name}}}']}{{/if}} + + + +
+ + {this.pagination()} +
+ ); + } + + pagination() { + const view = this.props.retrieved && this.props.retrieved['hydra:view']; + if (!view) return; + + const { + 'hydra:first': first, + 'hydra:previous': previous, + 'hydra:next': next, + 'hydra:last': last + } = view; + + return ( + + ); + } + + renderLinks = (type: string, items?: string | string[]) => { + if (!items) return null; + if (Array.isArray(items)) { + return items.map((item, i) => ( +
{this.renderLinks(type, item)}
+ )); + } + + return ( + {items} + ); + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(List); diff --git a/templates/typescript/components/foo/Show.tsx b/templates/typescript/components/foo/Show.tsx new file mode 100644 index 00000000..abf5a2d5 --- /dev/null +++ b/templates/typescript/components/foo/Show.tsx @@ -0,0 +1,143 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { RouteComponentProps } from 'react-router'; +import { Link, Redirect } from 'react-router-dom'; +import { TError, TDispatch } from '../../utils/types'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { retrieve, reset } from '../../actions/{{{lc}}}/show'; +import { del } from '../../actions/{{{lc}}}/delete'; +import { IDeleteState } from '../../types/{{{lc}}}/delete'; +import { IShowState } from '../../types/{{{lc}}}/show'; + +interface IShowProps { + retrieved: I{{{ucf}}} | null; + loading: boolean; + error: TError; + eventSource: EventSource | null; + deleteError: TError; + deleteLoading: boolean; + deleted: I{{{ucf}}} | null; +} + +interface IShowActions { + retrieve: (id: string) => any; + reset: (eventSource: EventSource | null) => any; + del: ({{{lc}}}: I{{{ucf}}} | null) => any; +} + +type TStore = { + {{{lc}}}: { + show: IShowState, + del: IDeleteState, + } +} + +const mapStateToProps: (state: TStore) => IShowProps = state => ({ + retrieved: state.{{{lc}}}.show.retrieved, + error: state.{{{lc}}}.show.error, + loading: state.{{{lc}}}.show.loading, + eventSource: state.{{{lc}}}.show.eventSource, + deleteError: state.{{{lc}}}.del.error, + deleteLoading: state.{{{lc}}}.del.loading, + deleted: state.{{{lc}}}.del.deleted +}); + +const mapDispatchToProps: (dispatch: TDispatch) => IShowActions = dispatch => ({ + retrieve: id => dispatch(retrieve(id)), + del: item => item && dispatch(del(item)), + reset: eventSource => dispatch(reset(eventSource)) +}); + +type TShowProps = RouteComponentProps & IShowProps & IShowActions; + +class Show extends Component { + componentDidMount() { + this.props.retrieve(decodeURIComponent(this.props.match.params.id)); + } + + componentWillUnmount() { + this.props.reset(this.props.eventSource); + } + + del = () => { + if (window.confirm('Are you sure you want to delete this item?')) + this.props.del(this.props.retrieved); + }; + + render() { + if (this.props.deleted) return ; + + const item = this.props.retrieved; + + return ( +
+

Show {item && item['@id']}

+ + {this.props.loading && ( +
+ Loading... +
+ )} + {this.props.error && ( +
+
+ )} + {this.props.deleteError && ( +
+
+ )} + + {item && ( + + + + + + + + +{{#each fields}} + + + + +{{/each}} + +
FieldValue
{{name}}{{#if reference}}{this.renderLinks('{{{reference.name}}}', item['{{{name}}}'])}{{else}}{item['{{{name}}}']}{{/if}}
+ )} + + Back to list + + {item && ( + + + + )} + +
+ ); + } + + renderLinks = (type: string, items?: string | string[]) => { + if (!items) return null; + if (Array.isArray(items)) { + return items.map((item, i) => ( +
{this.renderLinks(type, item)}
+ )); + } + + return ( + + {items} + + ); + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Show); diff --git a/templates/typescript/components/foo/Update.tsx b/templates/typescript/components/foo/Update.tsx new file mode 100644 index 00000000..f76c8400 --- /dev/null +++ b/templates/typescript/components/foo/Update.tsx @@ -0,0 +1,142 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { RouteComponentProps } from 'react-router'; +import { Link, Redirect } from 'react-router-dom'; +import { TError, TDispatch } from '../../utils/types'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import Form from './Form'; +import { retrieve, update, reset } from '../../actions/{{{lc}}}/update'; +import { del } from '../../actions/{{{lc}}}/delete'; +import { IUpdateState } from '../../types/{{{lc}}}/update'; +import { IDeleteState } from '../../types/{{{lc}}}/delete'; +import { ICreateState } from '../../types/{{{lc}}}/create'; + +interface IUpdateProps { + retrieved: I{{{ucf}}} | null; + retrieveLoading: boolean; + retrieveError: TError; + updateLoading: boolean; + updateError: TError; + deleteLoading: boolean; + deleteError: TError; + created: I{{{ucf}}} | null; + updated: I{{{ucf}}} | null; + deleted: I{{{ucf}}} | null; + eventSource: EventSource | null; +} + +interface IUpdateActions { + retrieve: (id: string) => any; + reset: (eventSource: EventSource | null) => any; + del: ({{{lc}}}: I{{{ucf}}} | null) => any; + update: ({{{lc}}}: I{{{ucf}}} | null, values: Partial) => any; +} + +type TStore = { + {{{lc}}}: { + update: IUpdateState, + del: IDeleteState, + create: ICreateState, + } +} + +const mapStateToProps: (state: TStore) => IUpdateProps = state => ({ + retrieved: state.{{{lc}}}.update.retrieved, + retrieveError: state.{{{lc}}}.update.retrieveError, + retrieveLoading: state.{{{lc}}}.update.retrieveLoading, + updateError: state.{{{lc}}}.update.updateError, + updateLoading: state.{{{lc}}}.update.updateLoading, + deleteError: state.{{{lc}}}.del.error, + deleteLoading: state.{{{lc}}}.del.loading, + eventSource: state.{{{lc}}}.update.eventSource, + created: state.{{{lc}}}.create.created, + deleted: state.{{{lc}}}.del.deleted, + updated: state.{{{lc}}}.update.updated +}); + +const mapDispatchToProps: (dispatch: TDispatch) => IUpdateActions = dispatch => ({ + retrieve: id => dispatch(retrieve(id)), + update: (item, values) => item && dispatch(update(item, values)), + del: item => item && dispatch(del(item)), + reset: eventSource => dispatch(reset(eventSource)) +}); + +type TUpdateProps = RouteComponentProps & IUpdateProps & IUpdateActions; + +class Update extends Component { + componentDidMount() { + this.props.retrieve(decodeURIComponent(this.props.match.params.id)); + } + + componentWillUnmount() { + this.props.reset(this.props.eventSource); + } + + del = () => { + if (window.confirm('Are you sure you want to delete this item?')) + this.props.del(this.props.retrieved); + }; + + render() { + if (this.props.deleted) return ; + + const item = this.props.updated ? this.props.updated : this.props.retrieved; + + return ( +
+

Edit {item && item['@id']}

+ + {this.props.created && ( +
+ {this.props.created['@id']} created. +
+ )} + {this.props.updated && ( +
+ {this.props.updated['@id']} updated. +
+ )} + {(this.props.retrieveLoading || + this.props.updateLoading || + this.props.deleteLoading) && ( +
+ Loading... +
+ )} + {this.props.retrieveError && ( +
+
+ )} + {this.props.updateError && ( +
+
+ )} + {this.props.deleteError && ( +
+
+ )} + + {item && ( +
this.props.update(item, values)} + initialValues={item} + /> + )} + + Back to list + + +
+ ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Update); diff --git a/templates/typescript/components/foo/index.tsx b/templates/typescript/components/foo/index.tsx new file mode 100644 index 00000000..e936e55e --- /dev/null +++ b/templates/typescript/components/foo/index.tsx @@ -0,0 +1,6 @@ +import Create from './Create'; +import List from './List'; +import Update from './Update'; +import Show from './Show'; + +export { Create, List, Update, Show }; diff --git a/templates/typescript/interface.ts b/templates/typescript/interface.ts deleted file mode 100644 index 04a5178f..00000000 --- a/templates/typescript/interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface {{{name}}} { - '@id'?: string; -{{#each fields}} - {{#if readonly}} readonly{{/if}} {{{name}}}{{#if notrequired}}?{{/if}}: {{{type}}}; -{{/each}} -} diff --git a/templates/typescript/interfaces/Collection.ts b/templates/typescript/interfaces/Collection.ts new file mode 100644 index 00000000..c5632712 --- /dev/null +++ b/templates/typescript/interfaces/Collection.ts @@ -0,0 +1,20 @@ +export interface IPagination { + '{{{hydraPrefix}}}first'?: string; + '{{{hydraPrefix}}}previous'?: string; + '{{{hydraPrefix}}}next'?: string; + '{{{hydraPrefix}}}last'?: string; +} + +export interface IPagedCollection { + '@context'?: string; + '@id'?: string; + '@type'?: string; + '{{{hydraPrefix}}}firstPage'?: string; + '{{{hydraPrefix}}}itemsPerPage'?: number; + '{{{hydraPrefix}}}lastPage'?: string; + '{{{hydraPrefix}}}member'?: T[]; + '{{{hydraPrefix}}}nextPage'?: string; + '{{{hydraPrefix}}}search'?: object; + '{{{hydraPrefix}}}totalItems'?: number; + '{{{hydraPrefix}}}view'?: IPagination; +} diff --git a/templates/typescript/interfaces/foo.ts b/templates/typescript/interfaces/foo.ts new file mode 100644 index 00000000..52ec64f0 --- /dev/null +++ b/templates/typescript/interfaces/foo.ts @@ -0,0 +1,8 @@ +import { IResource } from '../utils/types' + +export interface I{{{ucf}}} extends IResource { + id?: string; +{{#each fields}} + {{#if readonly}}readonly{{/if}} {{{name}}}?: {{{type}}}; +{{/each}} +} diff --git a/templates/typescript/reducers/foo/create.ts b/templates/typescript/reducers/foo/create.ts new file mode 100644 index 00000000..4d014477 --- /dev/null +++ b/templates/typescript/reducers/foo/create.ts @@ -0,0 +1,43 @@ +import { combineReducers } from 'redux'; +import { TError } from '../../utils/types'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { + {{{uc}}}_CREATE_ERROR, + {{{uc}}}_CREATE_LOADING, + {{{uc}}}_CREATE_SUCCESS, + IActionError, + IActionLoading, + IActionSuccess +} from '../../types/{{{lc}}}/create'; + +export function error(state: TError = null, action: IActionError) { + switch (action.type) { + case {{{uc}}}_CREATE_ERROR: + return action.error; + + default: + return state; + } +} + +export function loading(state: boolean = false, action: IActionLoading) { + switch (action.type) { + case {{{uc}}}_CREATE_LOADING: + return action.loading; + + default: + return state; + } +} + +export function created(state: I{{{ucf}}} | null = null, action: IActionSuccess) { + switch (action.type) { + case {{{uc}}}_CREATE_SUCCESS: + return action.created; + + default: + return state; + } +} + +export default combineReducers({ error, loading, created }); diff --git a/templates/typescript/reducers/foo/delete.ts b/templates/typescript/reducers/foo/delete.ts new file mode 100644 index 00000000..df11f61c --- /dev/null +++ b/templates/typescript/reducers/foo/delete.ts @@ -0,0 +1,43 @@ +import { combineReducers } from 'redux'; +import { TError } from '../../utils/types'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { + {{{uc}}}_DELETE_ERROR, + {{{uc}}}_DELETE_LOADING, + {{{uc}}}_DELETE_SUCCESS, + IActionError, + IActionLoading, + IActionSuccess +} from '../../types/{{{lc}}}/delete'; + +export function error(state: TError = null, action: IActionError) { + switch (action.type) { + case {{{uc}}}_DELETE_ERROR: + return action.error; + + default: + return state; + } +} + +export function loading(state: boolean = false, action: IActionLoading) { + switch (action.type) { + case {{{uc}}}_DELETE_LOADING: + return action.loading; + + default: + return state; + } +} + +export function deleted(state: I{{{ucf}}} | null = null, action: IActionSuccess) { + switch (action.type) { + case {{{uc}}}_DELETE_SUCCESS: + return action.deleted; + + default: + return state; + } +} + +export default combineReducers({ error, loading, deleted }); diff --git a/templates/typescript/reducers/foo/index.ts b/templates/typescript/reducers/foo/index.ts new file mode 100644 index 00000000..d57c1adb --- /dev/null +++ b/templates/typescript/reducers/foo/index.ts @@ -0,0 +1,8 @@ +import { combineReducers } from 'redux'; +import list from './list'; +import create from './create'; +import update from './update'; +import del from './delete'; +import show from './show'; + +export default combineReducers({ list, create, update, del, show }); diff --git a/templates/typescript/reducers/foo/list.ts b/templates/typescript/reducers/foo/list.ts new file mode 100644 index 00000000..2feb2fc2 --- /dev/null +++ b/templates/typescript/reducers/foo/list.ts @@ -0,0 +1,101 @@ +import { combineReducers } from 'redux'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { IPagedCollection } from '../../interfaces/Collection' +import { TError } from '../../utils/types' +import { + {{{uc}}}_LIST_ERROR, + {{{uc}}}_LIST_LOADING, + {{{uc}}}_LIST_MERCURE_DELETED, + {{{uc}}}_LIST_MERCURE_MESSAGE, + {{{uc}}}_LIST_MERCURE_OPEN, + {{{uc}}}_LIST_RESET, + {{{uc}}}_LIST_SUCCESS, + IActionError, + IActionLoading, + IActionSuccess, + IActionReset, + IActionMercureOpen, + IActionMercureDeleted, + IActionMercureMessage +} from '../../types/{{{lc}}}/list' + +export function error(state: TError = null, action: IActionError | IActionMercureDeleted | IActionReset) { + switch (action.type) { + case {{{uc}}}_LIST_ERROR: + return action.error; + + case {{{uc}}}_LIST_MERCURE_DELETED: + return `${action.retrieved['@id']} has been deleted by another user.`; + + case {{{uc}}}_LIST_RESET: + return null; + + default: + return state; + } +} + +export function loading(state: boolean = false, action: IActionLoading | IActionReset) { + switch (action.type) { + case {{{uc}}}_LIST_LOADING: + return action.loading; + + case {{{uc}}}_LIST_RESET: + return false; + + default: + return state; + } +} + +export function retrieved(state: IPagedCollection | null = null, action: IActionSuccess | IActionMercureMessage | IActionMercureDeleted | IActionReset) { + switch (action.type) { + case {{{uc}}}_LIST_SUCCESS: + return action.retrieved; + + case {{{uc}}}_LIST_RESET: + return null; + + case {{{uc}}}_LIST_MERCURE_MESSAGE: + if (!state || !Array.isArray(state['hydra:member'])) { + return state; + } + + return { + ...state, + 'hydra:member': state['hydra:member'].map(item => + item['@id'] === action.retrieved['@id'] ? action.retrieved : item + ) + }; + + case {{{uc}}}_LIST_MERCURE_DELETED: + if (!state || !Array.isArray(state['hydra:member'])) { + return state; + } + + return { + ...state, + 'hydra:member': state['hydra:member'].filter( + item => item['@id'] !== action.retrieved['@id'] + ) + }; + + default: + return state; + } +} + +export function eventSource(state: EventSource | null = null, action: IActionMercureOpen | IActionReset) { + switch (action.type) { + case {{{uc}}}_LIST_MERCURE_OPEN: + return action.eventSource; + + case {{{uc}}}_LIST_RESET: + return null; + + default: + return state; + } +} + +export default combineReducers({ error, loading, retrieved, eventSource }); diff --git a/templates/typescript/reducers/foo/show.ts b/templates/typescript/reducers/foo/show.ts new file mode 100644 index 00000000..e7218933 --- /dev/null +++ b/templates/typescript/reducers/foo/show.ts @@ -0,0 +1,77 @@ +import { combineReducers } from 'redux'; +import { TError } from '../../utils/types'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { + {{{uc}}}_SHOW_ERROR, + {{{uc}}}_SHOW_LOADING, + {{{uc}}}_SHOW_SUCCESS, + {{{uc}}}_SHOW_MERCURE_DELETED, + {{{uc}}}_SHOW_MERCURE_MESSAGE, + {{{uc}}}_SHOW_MERCURE_OPEN, + {{{uc}}}_SHOW_RESET, + IActionError, + IActionLoading, + IActionSuccess, + IActionReset, + IActionMercureOpen, + IActionMercureDeleted, + IActionMercureMessage +} from '../../types/{{{lc}}}/show'; + +export function error(state: TError = null, action: IActionError | IActionMercureDeleted | IActionReset) { + switch (action.type) { + case {{{uc}}}_SHOW_ERROR: + return action.error; + + case {{{uc}}}_SHOW_MERCURE_DELETED: + return `${action.retrieved['@id']} has been deleted by another user.`; + + case {{{uc}}}_SHOW_RESET: + return null; + + default: + return state; + } +} + +export function loading(state: boolean = false, action: IActionLoading | IActionReset) { + switch (action.type) { + case {{{uc}}}_SHOW_LOADING: + return action.loading; + + case {{{uc}}}_SHOW_RESET: + return false; + + default: + return state; + } +} + +export function retrieved(state: I{{{ucf}}} | null = null, action: IActionSuccess | IActionMercureMessage | IActionReset) { + switch (action.type) { + case {{{uc}}}_SHOW_SUCCESS: + case {{{uc}}}_SHOW_MERCURE_MESSAGE: + return action.retrieved; + + case {{{uc}}}_SHOW_RESET: + return null; + + default: + return state; + } +} + +export function eventSource(state: EventSource | null = null, action: IActionReset | IActionMercureOpen) { + switch (action.type) { + case {{{uc}}}_SHOW_MERCURE_OPEN: + return action.eventSource; + + case {{{uc}}}_SHOW_RESET: + return null; + + default: + return state; + } +} + +export default combineReducers({ error, loading, retrieved, eventSource }); diff --git a/templates/typescript/reducers/foo/update.ts b/templates/typescript/reducers/foo/update.ts new file mode 100644 index 00000000..7eabea95 --- /dev/null +++ b/templates/typescript/reducers/foo/update.ts @@ -0,0 +1,130 @@ +import { combineReducers } from 'redux'; +import { TError } from '../../utils/types'; +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { + {{{uc}}}_UPDATE_MERCURE_DELETED, + {{{uc}}}_UPDATE_MERCURE_MESSAGE, + {{{uc}}}_UPDATE_MERCURE_OPEN, + {{{uc}}}_UPDATE_RESET, + {{{uc}}}_UPDATE_RETRIEVE_ERROR, + {{{uc}}}_UPDATE_RETRIEVE_LOADING, + {{{uc}}}_UPDATE_RETRIEVE_SUCCESS, + {{{uc}}}_UPDATE_UPDATE_ERROR, + {{{uc}}}_UPDATE_UPDATE_LOADING, + {{{uc}}}_UPDATE_UPDATE_SUCCESS, + IActionRetrieveError, + IActionRetrieveLoading, + IActionRetrieveSuccess, + IActionUpdateError, + IActionUpdateLoading, + IActionUpdateSuccess, + IActionReset, + IActionMercureOpen, + IActionMercureDeleted, + IActionMercureMessage +} from '../../types/{{{lc}}}/update'; + +export function retrieveError(state: TError = null, action: IActionRetrieveError | IActionMercureDeleted | IActionReset) { + switch (action.type) { + case {{{uc}}}_UPDATE_RETRIEVE_ERROR: + return action.retrieveError; + + case {{{uc}}}_UPDATE_MERCURE_DELETED: + return `${action.retrieved['@id']} has been deleted by another user.`; + + case {{{uc}}}_UPDATE_RESET: + return null; + + default: + return state; + } +} + +export function retrieveLoading(state: boolean = false, action: IActionRetrieveLoading | IActionReset) { + switch (action.type) { + case {{{uc}}}_UPDATE_RETRIEVE_LOADING: + return action.retrieveLoading; + + case {{{uc}}}_UPDATE_RESET: + return false; + + default: + return state; + } +} + +export function retrieved(state: I{{{ucf}}} | null = null, action: IActionRetrieveSuccess | IActionMercureMessage | IActionReset) { + switch (action.type) { + case {{{uc}}}_UPDATE_RETRIEVE_SUCCESS: + case {{{uc}}}_UPDATE_MERCURE_MESSAGE: + return action.retrieved; + + case {{{uc}}}_UPDATE_RESET: + return null; + + default: + return state; + } +} + +export function updateError(state: TError = null, action: IActionUpdateError | IActionReset) { + switch (action.type) { + case {{{uc}}}_UPDATE_UPDATE_ERROR: + return action.updateError; + + case {{{uc}}}_UPDATE_RESET: + return null; + + default: + return state; + } +} + +export function updateLoading(state: boolean = false, action: IActionUpdateLoading | IActionReset) { + switch (action.type) { + case {{{uc}}}_UPDATE_UPDATE_LOADING: + return action.updateLoading; + + case {{{uc}}}_UPDATE_RESET: + return false; + + default: + return state; + } +} + +export function updated(state: I{{{ucf}}} | null = null, action: IActionUpdateSuccess | IActionReset) { + switch (action.type) { + case {{{uc}}}_UPDATE_UPDATE_SUCCESS: + return action.updated; + + case {{{uc}}}_UPDATE_RESET: + return null; + + default: + return state; + } +} + +export function eventSource(state: EventSource | null = null, action: IActionMercureOpen | IActionReset) { + switch (action.type) { + case {{{uc}}}_UPDATE_MERCURE_OPEN: + return action.eventSource; + + case {{{uc}}}_UPDATE_RESET: + return null; + + default: + return state; + } +} + +export default combineReducers({ + retrieveError, + retrieveLoading, + retrieved, + updateError, + updateLoading, + updated, + eventSource +}); diff --git a/templates/typescript/routes/foo.tsx b/templates/typescript/routes/foo.tsx new file mode 100644 index 00000000..c9d1306b --- /dev/null +++ b/templates/typescript/routes/foo.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Route } from 'react-router-dom'; +import { List, Create, Update, Show } from '../components/{{{lc}}}/'; + +export default [ + , + , + , + , + +]; diff --git a/templates/typescript/types/foo/create.ts b/templates/typescript/types/foo/create.ts new file mode 100644 index 00000000..67eebddc --- /dev/null +++ b/templates/typescript/types/foo/create.ts @@ -0,0 +1,26 @@ +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { TError } from '../../utils/types'; + +export const {{{uc}}}_CREATE_ERROR = '{{{uc}}}_CREATE_ERROR'; +export interface IActionError { + type: typeof {{{uc}}}_CREATE_ERROR; + error: TError; +} + +export const {{{uc}}}_CREATE_LOADING = '{{{uc}}}_CREATE_LOADING'; +export interface IActionLoading { + type: typeof {{{uc}}}_CREATE_LOADING; + loading: boolean; +} + +export const {{{uc}}}_CREATE_SUCCESS = '{{{uc}}}_CREATE_SUCCESS'; +export interface IActionSuccess { + type: typeof {{{uc}}}_CREATE_SUCCESS; + created: I{{{ucf}}} | null; +} + +export interface ICreateState { + error: TError; + loading: boolean; + created: I{{{ucf}}} | null; +} diff --git a/templates/typescript/types/foo/delete.ts b/templates/typescript/types/foo/delete.ts new file mode 100644 index 00000000..eea9dda9 --- /dev/null +++ b/templates/typescript/types/foo/delete.ts @@ -0,0 +1,26 @@ +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { TError } from '../../utils/types'; + +export const {{{uc}}}_DELETE_ERROR = '{{{uc}}}_DELETE_ERROR'; +export interface IActionError { + type: typeof {{{uc}}}_DELETE_ERROR; + error: TError; +} + +export const {{{uc}}}_DELETE_LOADING = '{{{uc}}}_DELETE_LOADING'; +export interface IActionLoading { + type: typeof {{{uc}}}_DELETE_LOADING; + loading: boolean; +} + +export const {{{uc}}}_DELETE_SUCCESS = '{{{uc}}}_DELETE_SUCCESS'; +export interface IActionSuccess { + type: typeof {{{uc}}}_DELETE_SUCCESS; + deleted: I{{{ucf}}} | null; +} + +export interface IDeleteState { + error: TError; + loading: boolean; + deleted: I{{{ucf}}} | null; +} diff --git a/templates/typescript/types/foo/list.ts b/templates/typescript/types/foo/list.ts new file mode 100644 index 00000000..795f3b5c --- /dev/null +++ b/templates/typescript/types/foo/list.ts @@ -0,0 +1,51 @@ +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { IPagedCollection } from '../../interfaces/Collection'; +import { TError, IResource } from '../../utils/types'; + +export const {{{uc}}}_LIST_ERROR = '{{{uc}}}_LIST_ERROR'; +export interface IActionError { + type: typeof {{{uc}}}_LIST_ERROR; + error: TError; +} + +export const {{{uc}}}_LIST_LOADING = '{{{uc}}}_LIST_LOADING'; +export interface IActionLoading { + type: typeof {{{uc}}}_LIST_LOADING; + loading: boolean; +} + +export const {{{uc}}}_LIST_SUCCESS = '{{{uc}}}_LIST_SUCCESS'; +export interface IActionSuccess { + type: typeof {{{uc}}}_LIST_SUCCESS; + retrieved: IPagedCollection; +} + +export const {{{uc}}}_LIST_RESET = '{{{uc}}}_LIST_RESET'; +export interface IActionReset { + type: typeof {{{uc}}}_LIST_RESET; +} + +export const {{{uc}}}_LIST_MERCURE_OPEN = '{{{uc}}}_LIST_MERCURE_OPEN'; +export interface IActionMercureOpen { + type: typeof {{{uc}}}_LIST_MERCURE_OPEN; + eventSource: EventSource; +} + +export const {{{uc}}}_LIST_MERCURE_DELETED = '{{{uc}}}_LIST_MERCURE_DELETED'; +export interface IActionMercureDeleted { + type: typeof {{{uc}}}_LIST_MERCURE_DELETED; +retrieved: IResource; +} + +export const {{{uc}}}_LIST_MERCURE_MESSAGE = '{{{uc}}}_LIST_MERCURE_MESSAGE'; +export interface IActionMercureMessage { + type: typeof {{{uc}}}_LIST_MERCURE_MESSAGE; + retrieved: I{{{ucf}}}; +} + +export interface IListState { + error: TError; + loading: boolean; + retrieved: IPagedCollection | null; + eventSource: EventSource | null; +} diff --git a/templates/typescript/types/foo/show.ts b/templates/typescript/types/foo/show.ts new file mode 100644 index 00000000..86c5f892 --- /dev/null +++ b/templates/typescript/types/foo/show.ts @@ -0,0 +1,50 @@ +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { TError, IResource } from '../../utils/types'; + +export const {{{uc}}}_SHOW_ERROR = '{{{uc}}}_SHOW_ERROR'; +export interface IActionError { + type: typeof {{{uc}}}_SHOW_ERROR; + error: TError; +} + +export const {{{uc}}}_SHOW_LOADING = '{{{uc}}}_SHOW_LOADING'; +export interface IActionLoading { + type: typeof {{{uc}}}_SHOW_LOADING; + loading: boolean; +} + +export const {{{uc}}}_SHOW_SUCCESS = '{{{uc}}}_SHOW_SUCCESS'; +export interface IActionSuccess { + type: typeof {{{uc}}}_SHOW_SUCCESS; + retrieved: I{{{ucf}}}; +} + +export const {{{uc}}}_SHOW_RESET = '{{{uc}}}_SHOW_RESET'; +export interface IActionReset { + type: typeof {{{uc}}}_SHOW_RESET; +} + +export const {{{uc}}}_SHOW_MERCURE_OPEN = '{{{uc}}}_SHOW_MERCURE_OPEN'; +export interface IActionMercureOpen { + type: typeof {{{uc}}}_SHOW_MERCURE_OPEN; + eventSource: EventSource; +} + +export const {{{uc}}}_SHOW_MERCURE_DELETED = '{{{uc}}}_SHOW_MERCURE_DELETED'; +export interface IActionMercureDeleted { + type: typeof {{{uc}}}_SHOW_MERCURE_DELETED; + retrieved: IResource; +} + +export const {{{uc}}}_SHOW_MERCURE_MESSAGE = '{{{uc}}}_SHOW_MERCURE_MESSAGE'; +export interface IActionMercureMessage { + type: typeof {{{uc}}}_SHOW_MERCURE_MESSAGE; + retrieved: I{{{ucf}}}; +} + +export interface IShowState { + error: TError; + loading: boolean; + retrieved: I{{{ucf}}} | null; + eventSource: EventSource | null; +} diff --git a/templates/typescript/types/foo/update.ts b/templates/typescript/types/foo/update.ts new file mode 100644 index 00000000..4f6e1d12 --- /dev/null +++ b/templates/typescript/types/foo/update.ts @@ -0,0 +1,71 @@ +import { I{{{ucf}}} } from '../../interfaces/{{{ucf}}}'; +import { TError, IResource } from '../../utils/types'; + +export const {{{uc}}}_UPDATE_RETRIEVE_ERROR = '{{{uc}}}_UPDATE_RETRIEVE_ERROR'; +export interface IActionRetrieveError { + type: typeof {{{uc}}}_UPDATE_RETRIEVE_ERROR; + retrieveError: TError; +} + +export const {{{uc}}}_UPDATE_RETRIEVE_LOADING = '{{{uc}}}_UPDATE_RETRIEVE_LOADING'; +export interface IActionRetrieveLoading { + type: typeof {{{uc}}}_UPDATE_RETRIEVE_LOADING; + retrieveLoading: boolean; +} + +export const {{{uc}}}_UPDATE_RETRIEVE_SUCCESS = '{{{uc}}}_UPDATE_RETRIEVE_SUCCESS'; +export interface IActionRetrieveSuccess { + type: typeof {{{uc}}}_UPDATE_RETRIEVE_SUCCESS; + retrieved: I{{{ucf}}}; +} + +export const {{{uc}}}_UPDATE_UPDATE_ERROR = '{{{uc}}}_UPDATE_UPDATE_ERROR'; +export interface IActionUpdateError { + type: typeof {{{uc}}}_UPDATE_UPDATE_ERROR; + updateError: TError; +} + +export const {{{uc}}}_UPDATE_UPDATE_LOADING = '{{{uc}}}_UPDATE_UPDATE_LOADING'; +export interface IActionUpdateLoading { + type: typeof {{{uc}}}_UPDATE_UPDATE_LOADING; + updateLoading: boolean; +} + +export const {{{uc}}}_UPDATE_UPDATE_SUCCESS = '{{{uc}}}_UPDATE_UPDATE_SUCCESS'; +export interface IActionUpdateSuccess { + type: typeof {{{uc}}}_UPDATE_UPDATE_SUCCESS; + updated: I{{{ucf}}}; +} + +export const {{{uc}}}_UPDATE_RESET = '{{{uc}}}_UPDATE_RESET'; +export interface IActionReset { + type: typeof {{{uc}}}_UPDATE_RESET; +} + +export const {{{uc}}}_UPDATE_MERCURE_OPEN = '{{{uc}}}_UPDATE_MERCURE_OPEN'; +export interface IActionMercureOpen { + type: typeof {{{uc}}}_UPDATE_MERCURE_OPEN; + eventSource: EventSource; +} + +export const {{{uc}}}_UPDATE_MERCURE_DELETED = '{{{uc}}}_UPDATE_MERCURE_DELETED'; +export interface IActionMercureDeleted { + type: typeof {{{uc}}}_UPDATE_MERCURE_DELETED; + retrieved: IResource; +} + +export const {{{uc}}}_UPDATE_MERCURE_MESSAGE = '{{{uc}}}_UPDATE_MERCURE_MESSAGE'; +export interface IActionMercureMessage { + type: typeof {{{uc}}}_UPDATE_MERCURE_MESSAGE; + retrieved: I{{{ucf}}}; +} + +export interface IUpdateState { + retrieveError: TError; + retrieveLoading: boolean; + retrieved: I{{{ucf}}} | null; + updateError: TError; + updateLoading: boolean; + updated: I{{{ucf}}} | null; + eventSource: EventSource | null; +} diff --git a/templates/typescript/utils/dataAccess.ts b/templates/typescript/utils/dataAccess.ts new file mode 100644 index 00000000..098a631e --- /dev/null +++ b/templates/typescript/utils/dataAccess.ts @@ -0,0 +1,90 @@ +import { ENTRYPOINT } from '../config/entrypoint'; +import { SubmissionError } from 'redux-form'; + +const MIME_TYPE = 'application/ld+json'; + +export function fetchApi (id: string, options: RequestInit = {}) { + if (!(options.headers instanceof Headers)) options.headers = new Headers(options.headers); + if (null === options.headers.get('Accept')) + options.headers.set('Accept', MIME_TYPE); + + if ( + 'undefined' !== options.body && + !(options.body instanceof FormData) && + null === options.headers.get('Content-Type') + ) + options.headers.set('Content-Type', MIME_TYPE); + + return fetch(String(new URL(id, ENTRYPOINT)), options).then(response => { + if (response.ok) return response; + + return response.json().then( + json => { + const error = + json['hydra:description'] || + json['hydra:title'] || + 'An error occurred.'; + if (!json.violations) throw Error(error); + + const violations: { propertyPath: string; message: string; }[] = json.violations + const errors = violations + .reduce((errors, violation) => { + if (errors[violation.propertyPath]) { + errors[violation.propertyPath] += '\n' + violation.message + } else { + errors[violation.propertyPath] = violation.message + } + + return errors + }, {_error: error} as {_error: string; [key: string]: string; }); + + throw new SubmissionError(errors); + }, + () => { + throw new Error(response.statusText || 'An error occurred.'); + } + ); + }); +} + +export function mercureSubscribe(url: URL, topics: string[]) { + topics.forEach(topic => + url.searchParams.append('topic', String(new URL(topic, ENTRYPOINT))) + ); + + return new EventSource(url.toString()); +} + +export function normalize(data: A) { + if (data.hasOwnProperty('hydra:member') && data['hydra:member']) { + // Normalize items in collections + data['hydra:member'] = data['hydra:member'].map(item => normalize(item)); + + return data; + } + + // Flatten nested documents + return Object + .entries(data) + .reduce( + (a, [key, value]) => { + a[key] = Array.isArray(value) + ? value.map(v => v && v.hasOwnProperty('@id') ? v['@id'] : v) + : value && value.hasOwnProperty('@id') ? value['@id'] : value + + return a + }, + {} as any, + ); +} + +export function extractHubURL(response: Response) { + const linkHeader = response.headers.get('Link'); + if (!linkHeader) return null; + + const matches = linkHeader.match( + /<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/ + ); + + return matches && matches[1] ? new URL(matches[1], ENTRYPOINT) : null; +} diff --git a/templates/typescript/utils/types.ts b/templates/typescript/utils/types.ts new file mode 100644 index 00000000..dd103344 --- /dev/null +++ b/templates/typescript/utils/types.ts @@ -0,0 +1,10 @@ +import { AnyAction } from 'redux'; +import { ThunkDispatch as Dispatch } from 'redux-thunk'; + +export type TError = Error | string | null; + +export type TDispatch = Dispatch<{}, {}, AnyAction>; + +export interface IResource { + '@id': string; +} From 69f7cbfe3c683fffe4e8989382428a177f75b0a2 Mon Sep 17 00:00:00 2001 From: Valery Vargin Date: Tue, 17 Dec 2019 21:07:24 +0300 Subject: [PATCH 2/2] Interfaces generator is useful in certain cases #192 --- src/generators.js | 7 +- ...nerator.js => ReactTypescriptGenerator.js} | 4 +- ...st.js => ReactTypescriptGenerator.test.js} | 4 +- .../TypescriptInterfaceGenerator.js | 94 ++++++++++ .../TypescriptInterfaceGenerator.test.js | 173 ++++++++++++++++++ .../actions/foo/create.ts | 0 .../actions/foo/delete.ts | 0 .../actions/foo/list.ts | 0 .../actions/foo/show.ts | 0 .../actions/foo/update.ts | 0 .../components/foo/Create.tsx | 0 .../components/foo/Form.tsx | 0 .../components/foo/List.tsx | 0 .../components/foo/Show.tsx | 0 .../components/foo/Update.tsx | 0 .../components/foo/index.tsx | 0 .../interfaces/Collection.ts | 0 .../interfaces/foo.ts | 0 .../reducers/foo/create.ts | 0 .../reducers/foo/delete.ts | 0 .../reducers/foo/index.ts | 0 .../reducers/foo/list.ts | 0 .../reducers/foo/show.ts | 0 .../reducers/foo/update.ts | 0 .../routes/foo.tsx | 0 .../types/foo/create.ts | 0 .../types/foo/delete.ts | 0 .../types/foo/list.ts | 0 .../types/foo/show.ts | 0 .../types/foo/update.ts | 0 .../utils/dataAccess.ts | 0 .../utils/types.ts | 0 templates/typescript/interface.ts | 6 + 33 files changed, 282 insertions(+), 6 deletions(-) rename src/generators/{TypescriptGenerator.js => ReactTypescriptGenerator.js} (97%) rename src/generators/{TypescriptGenerator.test.js => ReactTypescriptGenerator.test.js} (94%) create mode 100644 src/generators/TypescriptInterfaceGenerator.js create mode 100644 src/generators/TypescriptInterfaceGenerator.test.js rename templates/{typescript => react-typescript}/actions/foo/create.ts (100%) rename templates/{typescript => react-typescript}/actions/foo/delete.ts (100%) rename templates/{typescript => react-typescript}/actions/foo/list.ts (100%) rename templates/{typescript => react-typescript}/actions/foo/show.ts (100%) rename templates/{typescript => react-typescript}/actions/foo/update.ts (100%) rename templates/{typescript => react-typescript}/components/foo/Create.tsx (100%) rename templates/{typescript => react-typescript}/components/foo/Form.tsx (100%) rename templates/{typescript => react-typescript}/components/foo/List.tsx (100%) rename templates/{typescript => react-typescript}/components/foo/Show.tsx (100%) rename templates/{typescript => react-typescript}/components/foo/Update.tsx (100%) rename templates/{typescript => react-typescript}/components/foo/index.tsx (100%) rename templates/{typescript => react-typescript}/interfaces/Collection.ts (100%) rename templates/{typescript => react-typescript}/interfaces/foo.ts (100%) rename templates/{typescript => react-typescript}/reducers/foo/create.ts (100%) rename templates/{typescript => react-typescript}/reducers/foo/delete.ts (100%) rename templates/{typescript => react-typescript}/reducers/foo/index.ts (100%) rename templates/{typescript => react-typescript}/reducers/foo/list.ts (100%) rename templates/{typescript => react-typescript}/reducers/foo/show.ts (100%) rename templates/{typescript => react-typescript}/reducers/foo/update.ts (100%) rename templates/{typescript => react-typescript}/routes/foo.tsx (100%) rename templates/{typescript => react-typescript}/types/foo/create.ts (100%) rename templates/{typescript => react-typescript}/types/foo/delete.ts (100%) rename templates/{typescript => react-typescript}/types/foo/list.ts (100%) rename templates/{typescript => react-typescript}/types/foo/show.ts (100%) rename templates/{typescript => react-typescript}/types/foo/update.ts (100%) rename templates/{typescript => react-typescript}/utils/dataAccess.ts (100%) rename templates/{typescript => react-typescript}/utils/types.ts (100%) create mode 100644 templates/typescript/interface.ts diff --git a/src/generators.js b/src/generators.js index e2f15a99..ca460942 100644 --- a/src/generators.js +++ b/src/generators.js @@ -2,7 +2,8 @@ import AdminOnRestGenerator from "./generators/AdminOnRestGenerator"; import NextGenerator from "./generators/NextGenerator"; import ReactGenerator from "./generators/ReactGenerator"; import ReactNativeGenerator from "./generators/ReactNativeGenerator"; -import TypescriptGenerator from "./generators/TypescriptGenerator"; +import ReactTypescriptGenerator from "./generators/ReactTypescriptGenerator"; +import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenerator"; import VueGenerator from "./generators/VueGenerator"; import VuetifyGenerator from "./generators/VuetifyGenerator"; import QuasarGenerator from "./generators/QuasarGenerator"; @@ -23,7 +24,9 @@ export default function generators(generator = "react") { case "react-native": return wrap(ReactNativeGenerator); case "typescript": - return wrap(TypescriptGenerator); + return wrap(TypescriptInterfaceGenerator); + case "react-typescript": + return wrap(ReactTypescriptGenerator); case "vue": return wrap(VueGenerator); case "vuetify": diff --git a/src/generators/TypescriptGenerator.js b/src/generators/ReactTypescriptGenerator.js similarity index 97% rename from src/generators/TypescriptGenerator.js rename to src/generators/ReactTypescriptGenerator.js index cf35e539..ff0b9612 100644 --- a/src/generators/TypescriptGenerator.js +++ b/src/generators/ReactTypescriptGenerator.js @@ -1,11 +1,11 @@ import chalk from "chalk"; import BaseGenerator from "./BaseGenerator"; -export default class TypescriptGenerator extends BaseGenerator { +export default class ReactTypescriptGenerator extends BaseGenerator { constructor(params) { super(params); - this.registerTemplates("typescript/", [ + this.registerTemplates("react-typescript/", [ // actions "actions/foo/create.ts", "actions/foo/delete.ts", diff --git a/src/generators/TypescriptGenerator.test.js b/src/generators/ReactTypescriptGenerator.test.js similarity index 94% rename from src/generators/TypescriptGenerator.test.js rename to src/generators/ReactTypescriptGenerator.test.js index 98275a58..31375f3d 100644 --- a/src/generators/TypescriptGenerator.test.js +++ b/src/generators/ReactTypescriptGenerator.test.js @@ -1,10 +1,10 @@ import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib"; import fs from "fs"; import tmp from "tmp"; -import TypescriptGenerator from "./TypescriptGenerator"; +import ReactTypescriptGenerator from "./ReactTypescriptGenerator"; test("Generate a Typescript React app", () => { - const generator = new TypescriptGenerator({ + const generator = new ReactTypescriptGenerator({ hydraPrefix: "hydra:", templateDirectory: `${__dirname}/../../templates` }); diff --git a/src/generators/TypescriptInterfaceGenerator.js b/src/generators/TypescriptInterfaceGenerator.js new file mode 100644 index 00000000..d8f4e37a --- /dev/null +++ b/src/generators/TypescriptInterfaceGenerator.js @@ -0,0 +1,94 @@ +import BaseGenerator from "./BaseGenerator"; + +export default class TypescriptInterfaceGenerator extends BaseGenerator { + constructor(params) { + super(params); + + this.registerTemplates(`typescript/`, ["interface.ts"]); + } + + help(resource) { + console.log( + 'Interface for the "%s" resource type has been generated!', + resource.title + ); + } + + generate(api, resource, dir) { + const dest = `${dir}/interfaces`; + const { fields, imports } = this.parseFields(resource); + + this.createDir(dest, false); + this.createFile( + "interface.ts", + `${dest}/${resource.title.toLowerCase()}.ts`, + { + fields, + imports, + name: resource.title + } + ); + } + + getDescription(field) { + return field.description ? field.description.replace(/"/g, "'") : ""; + } + + parseFields(resource) { + const fields = {}; + + for (let field of resource.writableFields) { + fields[field.name] = { + notrequired: !field.required, + name: field.name, + type: this.getType(field), + description: this.getDescription(field), + readonly: false, + reference: field.reference + }; + } + + for (let field of resource.readableFields) { + if (fields[field.name] !== undefined) { + continue; + } + + fields[field.name] = { + notrequired: !field.required, + name: field.name, + type: this.getType(field), + description: this.getDescription(field), + readonly: true, + reference: field.reference + }; + } + + // If id is not present, add it manually with default values + if (!("id" in fields)) { + fields["id"] = { + notrequired: true, + name: "id", + type: "string", + description: null, + readonly: false + }; + } + + // Parse fields to add relevant imports, required for Typescript + const fieldsArray = Object.keys(fields).map(e => fields[e]); + const imports = {}; + + for (const field of fieldsArray) { + if (field.reference) { + imports[field.type] = { + type: field.type, + file: "./" + field.type.toLowerCase() + }; + } + } + + const importsArray = Object.keys(imports).map(e => imports[e]); + + return { fields: fieldsArray, imports: importsArray }; + } +} diff --git a/src/generators/TypescriptInterfaceGenerator.test.js b/src/generators/TypescriptInterfaceGenerator.test.js new file mode 100644 index 00000000..a60375ab --- /dev/null +++ b/src/generators/TypescriptInterfaceGenerator.test.js @@ -0,0 +1,173 @@ +import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib"; +import fs from "fs"; +import tmp from "tmp"; +import TypescriptInterfaceGenerator from "./TypescriptInterfaceGenerator"; + +test("Generate a typescript interface", () => { + const generator = new TypescriptInterfaceGenerator({ + templateDirectory: `${__dirname}/../../templates` + }); + const tmpobj = tmp.dirSync({ unsafeCleanup: true }); + + const resource = new Resource("abc", "http://example.com/foos", { + id: "foo", + title: "Foo", + readableFields: [ + new Field("bar", { + id: "http://schema.org/url", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: true, + description: "An URL" + }) + ], + writableFields: [ + new Field("foo", { + id: "http://schema.org/url", + range: "http://www.w3.org/2001/XMLSchema#datetime", + reference: null, + required: true, + description: "An URL" + }), + new Field("foobar", { + id: "http://schema.org/url", + range: undefined, + reference: new Resource("foobar", "http://example.com/FooBar", { + title: "FooBar" + }), + required: false + }) + ] + }); + const api = new Api("http://example.com", { + entrypoint: "http://example.com:8080", + title: "My API", + resources: [resource] + }); + generator.generate(api, resource, tmpobj.name); + + expect(fs.existsSync(tmpobj.name + "/interfaces/foo.ts")).toBe(true); + + const res = `export interface Foo { + '@id'?: string; + foo: any; + foobar?: string[]; + readonly bar: string; + id?: string; +} +`; + expect( + fs.readFileSync(tmpobj.name + "/interfaces/foo.ts").toString() + ).toEqual(res); + + tmpobj.removeCallback(); +}); + +test("Generate a typescript interface without references to other interfaces", () => { + const generator = new TypescriptInterfaceGenerator({ + templateDirectory: `${__dirname}/../../templates` + }); + const tmpobj = tmp.dirSync({ unsafeCleanup: true }); + + const resource = new Resource("abc", "http://example.com/foos", { + id: "foo", + title: "Foo", + readableFields: [ + new Field("bar", { + id: "http://schema.org/url", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: true, + description: "An URL" + }) + ], + writableFields: [ + new Field("foo", { + id: "http://schema.org/url", + range: "http://www.w3.org/2001/XMLSchema#datetime", + reference: null, + required: true, + description: "An URL" + }) + ] + }); + const api = new Api("http://example.com", { + entrypoint: "http://example.com:8080", + title: "My API", + resources: [resource] + }); + generator.generate(api, resource, tmpobj.name); + + expect(fs.existsSync(tmpobj.name + "/interfaces/foo.ts")).toBe(true); + + const res = `export interface Foo { + '@id'?: string; + foo: any; + readonly bar: string; + id?: string; +} +`; + expect( + fs.readFileSync(tmpobj.name + "/interfaces/foo.ts").toString() + ).toEqual(res); + + tmpobj.removeCallback(); +}); + +test("Generate a typescript interface with an explicit id field in the readableFields", () => { + const generator = new TypescriptInterfaceGenerator({ + templateDirectory: `${__dirname}/../../templates` + }); + const tmpobj = tmp.dirSync({ unsafeCleanup: true }); + + const resource = new Resource("abc", "http://example.com/foos", { + id: "foo", + title: "Foo", + readableFields: [ + new Field("bar", { + id: "http://schema.org/url", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: true, + description: "An URL" + }), + new Field("id", { + id: "http://schema.org/url", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: false, + description: "Id" + }) + ], + writableFields: [ + new Field("foo", { + id: "http://schema.org/url", + range: "http://www.w3.org/2001/XMLSchema#datetime", + reference: null, + required: true, + description: "An URL" + }) + ] + }); + const api = new Api("http://example.com", { + entrypoint: "http://example.com:8080", + title: "My API", + resources: [resource] + }); + generator.generate(api, resource, tmpobj.name); + + expect(fs.existsSync(tmpobj.name + "/interfaces/foo.ts")).toBe(true); + + const res = `export interface Foo { + '@id'?: string; + foo: any; + readonly bar: string; + readonly id?: string; +} +`; + expect( + fs.readFileSync(tmpobj.name + "/interfaces/foo.ts").toString() + ).toEqual(res); + + tmpobj.removeCallback(); +}); diff --git a/templates/typescript/actions/foo/create.ts b/templates/react-typescript/actions/foo/create.ts similarity index 100% rename from templates/typescript/actions/foo/create.ts rename to templates/react-typescript/actions/foo/create.ts diff --git a/templates/typescript/actions/foo/delete.ts b/templates/react-typescript/actions/foo/delete.ts similarity index 100% rename from templates/typescript/actions/foo/delete.ts rename to templates/react-typescript/actions/foo/delete.ts diff --git a/templates/typescript/actions/foo/list.ts b/templates/react-typescript/actions/foo/list.ts similarity index 100% rename from templates/typescript/actions/foo/list.ts rename to templates/react-typescript/actions/foo/list.ts diff --git a/templates/typescript/actions/foo/show.ts b/templates/react-typescript/actions/foo/show.ts similarity index 100% rename from templates/typescript/actions/foo/show.ts rename to templates/react-typescript/actions/foo/show.ts diff --git a/templates/typescript/actions/foo/update.ts b/templates/react-typescript/actions/foo/update.ts similarity index 100% rename from templates/typescript/actions/foo/update.ts rename to templates/react-typescript/actions/foo/update.ts diff --git a/templates/typescript/components/foo/Create.tsx b/templates/react-typescript/components/foo/Create.tsx similarity index 100% rename from templates/typescript/components/foo/Create.tsx rename to templates/react-typescript/components/foo/Create.tsx diff --git a/templates/typescript/components/foo/Form.tsx b/templates/react-typescript/components/foo/Form.tsx similarity index 100% rename from templates/typescript/components/foo/Form.tsx rename to templates/react-typescript/components/foo/Form.tsx diff --git a/templates/typescript/components/foo/List.tsx b/templates/react-typescript/components/foo/List.tsx similarity index 100% rename from templates/typescript/components/foo/List.tsx rename to templates/react-typescript/components/foo/List.tsx diff --git a/templates/typescript/components/foo/Show.tsx b/templates/react-typescript/components/foo/Show.tsx similarity index 100% rename from templates/typescript/components/foo/Show.tsx rename to templates/react-typescript/components/foo/Show.tsx diff --git a/templates/typescript/components/foo/Update.tsx b/templates/react-typescript/components/foo/Update.tsx similarity index 100% rename from templates/typescript/components/foo/Update.tsx rename to templates/react-typescript/components/foo/Update.tsx diff --git a/templates/typescript/components/foo/index.tsx b/templates/react-typescript/components/foo/index.tsx similarity index 100% rename from templates/typescript/components/foo/index.tsx rename to templates/react-typescript/components/foo/index.tsx diff --git a/templates/typescript/interfaces/Collection.ts b/templates/react-typescript/interfaces/Collection.ts similarity index 100% rename from templates/typescript/interfaces/Collection.ts rename to templates/react-typescript/interfaces/Collection.ts diff --git a/templates/typescript/interfaces/foo.ts b/templates/react-typescript/interfaces/foo.ts similarity index 100% rename from templates/typescript/interfaces/foo.ts rename to templates/react-typescript/interfaces/foo.ts diff --git a/templates/typescript/reducers/foo/create.ts b/templates/react-typescript/reducers/foo/create.ts similarity index 100% rename from templates/typescript/reducers/foo/create.ts rename to templates/react-typescript/reducers/foo/create.ts diff --git a/templates/typescript/reducers/foo/delete.ts b/templates/react-typescript/reducers/foo/delete.ts similarity index 100% rename from templates/typescript/reducers/foo/delete.ts rename to templates/react-typescript/reducers/foo/delete.ts diff --git a/templates/typescript/reducers/foo/index.ts b/templates/react-typescript/reducers/foo/index.ts similarity index 100% rename from templates/typescript/reducers/foo/index.ts rename to templates/react-typescript/reducers/foo/index.ts diff --git a/templates/typescript/reducers/foo/list.ts b/templates/react-typescript/reducers/foo/list.ts similarity index 100% rename from templates/typescript/reducers/foo/list.ts rename to templates/react-typescript/reducers/foo/list.ts diff --git a/templates/typescript/reducers/foo/show.ts b/templates/react-typescript/reducers/foo/show.ts similarity index 100% rename from templates/typescript/reducers/foo/show.ts rename to templates/react-typescript/reducers/foo/show.ts diff --git a/templates/typescript/reducers/foo/update.ts b/templates/react-typescript/reducers/foo/update.ts similarity index 100% rename from templates/typescript/reducers/foo/update.ts rename to templates/react-typescript/reducers/foo/update.ts diff --git a/templates/typescript/routes/foo.tsx b/templates/react-typescript/routes/foo.tsx similarity index 100% rename from templates/typescript/routes/foo.tsx rename to templates/react-typescript/routes/foo.tsx diff --git a/templates/typescript/types/foo/create.ts b/templates/react-typescript/types/foo/create.ts similarity index 100% rename from templates/typescript/types/foo/create.ts rename to templates/react-typescript/types/foo/create.ts diff --git a/templates/typescript/types/foo/delete.ts b/templates/react-typescript/types/foo/delete.ts similarity index 100% rename from templates/typescript/types/foo/delete.ts rename to templates/react-typescript/types/foo/delete.ts diff --git a/templates/typescript/types/foo/list.ts b/templates/react-typescript/types/foo/list.ts similarity index 100% rename from templates/typescript/types/foo/list.ts rename to templates/react-typescript/types/foo/list.ts diff --git a/templates/typescript/types/foo/show.ts b/templates/react-typescript/types/foo/show.ts similarity index 100% rename from templates/typescript/types/foo/show.ts rename to templates/react-typescript/types/foo/show.ts diff --git a/templates/typescript/types/foo/update.ts b/templates/react-typescript/types/foo/update.ts similarity index 100% rename from templates/typescript/types/foo/update.ts rename to templates/react-typescript/types/foo/update.ts diff --git a/templates/typescript/utils/dataAccess.ts b/templates/react-typescript/utils/dataAccess.ts similarity index 100% rename from templates/typescript/utils/dataAccess.ts rename to templates/react-typescript/utils/dataAccess.ts diff --git a/templates/typescript/utils/types.ts b/templates/react-typescript/utils/types.ts similarity index 100% rename from templates/typescript/utils/types.ts rename to templates/react-typescript/utils/types.ts diff --git a/templates/typescript/interface.ts b/templates/typescript/interface.ts new file mode 100644 index 00000000..04a5178f --- /dev/null +++ b/templates/typescript/interface.ts @@ -0,0 +1,6 @@ +export interface {{{name}}} { + '@id'?: string; +{{#each fields}} + {{#if readonly}} readonly{{/if}} {{{name}}}{{#if notrequired}}?{{/if}}: {{{type}}}; +{{/each}} +}