Skip to content

Commit

Permalink
feat(core): add relationships ops to resources
Browse files Browse the repository at this point in the history
  • Loading branch information
pviti committed Feb 10, 2022
1 parent 17c55f2 commit 15240c9
Show file tree
Hide file tree
Showing 208 changed files with 6,820 additions and 366 deletions.
96 changes: 63 additions & 33 deletions gen/generator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-disable no-console */

import apiSchema, { Resource, Operation, Component, Cardinality } from './schema'
import fs from 'fs'
import path from 'path'
import _ from 'lodash'
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync } from 'fs'
import { basename } from 'path'
import { capitalize, snakeCase } from 'lodash'
import { inspect } from 'util'

// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand All @@ -28,10 +28,10 @@ const global: {

const loadTemplates = (): void => {
const tplDir = './gen/templates'
const tplList = fs.readdirSync(tplDir, { encoding: 'utf-8' }).filter(f => f.endsWith('.tpl'))
const tplList = readdirSync(tplDir, { encoding: 'utf-8' }).filter(f => f.endsWith('.tpl'))
tplList.forEach(t => {
const tplName = path.basename(t).replace('.tpl', '')
const tpl = fs.readFileSync(`${tplDir}/${tplName}.tpl`, { encoding: 'utf-8' })
const tplName = basename(t).replace('.tpl', '')
const tpl = readFileSync(`${tplDir}/${tplName}.tpl`, { encoding: 'utf-8' })
templates[tplName] = tpl
})
}
Expand All @@ -42,7 +42,7 @@ const generate = async (localSchema?: boolean) => {
console.log('>> Local schema: ' + (localSchema || false) + '\n')

const schemaPath = localSchema ? 'gen/openapi.json' : await apiSchema.download()
if (!fs.existsSync(schemaPath)) {
if (!existsSync(schemaPath)) {
console.log('Cannot find schema file: ' + schemaPath)
return
}
Expand All @@ -56,13 +56,13 @@ const generate = async (localSchema?: boolean) => {

// Initialize source dir
const resDir = 'src/resources'
if (fs.existsSync(resDir)) fs.rmSync(resDir, { recursive: true })
fs.mkdirSync(resDir, { recursive: true })
if (existsSync(resDir)) rmSync(resDir, { recursive: true })
mkdirSync(resDir, { recursive: true })

// Initialize test dir
const testDir = 'specs/resources'
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true })
fs.mkdirSync(testDir, { recursive: true })
if (existsSync(testDir)) rmSync(testDir, { recursive: true })
mkdirSync(testDir, { recursive: true })


const resources: { [key: string]: ApiRes } = {}
Expand All @@ -72,11 +72,11 @@ const generate = async (localSchema?: boolean) => {
const name = Inflector.pluralize(Inflector.camelize(type)) as string

const tplRes = generateResource(type, name, res)
fs.writeFileSync(`${resDir}/${type}.ts`, tplRes)
writeFileSync(`${resDir}/${type}.ts`, tplRes)
console.log('Generated resource ' + name)

const tplSpec = generateSpec(type, name, res)
fs.writeFileSync(`${testDir}/${type}.spec.ts`, tplSpec)
writeFileSync(`${testDir}/${type}.spec.ts`, tplSpec)
console.log('Generated spec ' + name)

resources[type] = {
Expand Down Expand Up @@ -122,7 +122,7 @@ const tabsString = (num: number): string => {

const updateSdkInterfaces = (resources: { [key: string]: ApiRes }): void => {

const cl = fs.readFileSync('src/commercelayer.ts', { encoding: 'utf-8' })
const cl = readFileSync('src/commercelayer.ts', { encoding: 'utf-8' })

const lines = cl.split('\n')

Expand Down Expand Up @@ -173,7 +173,7 @@ const updateSdkInterfaces = (resources: { [key: string]: ApiRes }): void => {
// console.log(definitions)
// console.log(initializations)

fs.writeFileSync('src/commercelayer.ts', lines.join('\n'), { encoding: 'utf-8' })
writeFileSync('src/commercelayer.ts', lines.join('\n'), { encoding: 'utf-8' })

console.log('API interfaces generated.')

Expand All @@ -182,7 +182,7 @@ const updateSdkInterfaces = (resources: { [key: string]: ApiRes }): void => {

const updateModelTypes = (resources: { [key: string]: ApiRes }): void => {

const cl = fs.readFileSync('src/model.ts', { encoding: 'utf-8' })
const cl = readFileSync('src/model.ts', { encoding: 'utf-8' })

const lines = cl.split('\n')

Expand All @@ -207,7 +207,7 @@ const updateModelTypes = (resources: { [key: string]: ApiRes }): void => {
lines.splice(expStartIdx, expStopIdx - expStartIdx, ...exports)


fs.writeFileSync('src/model.ts', lines.join('\n'), { encoding: 'utf-8' })
writeFileSync('src/model.ts', lines.join('\n'), { encoding: 'utf-8' })

console.log('Model types generated.')

Expand All @@ -216,7 +216,7 @@ const updateModelTypes = (resources: { [key: string]: ApiRes }): void => {

const updateApiResources = (resources: { [key: string]: ApiRes }): void => {

const cl = fs.readFileSync('src/api.ts', { encoding: 'utf-8' })
const cl = readFileSync('src/api.ts', { encoding: 'utf-8' })

const lines = cl.split('\n')

Expand Down Expand Up @@ -257,7 +257,7 @@ const updateApiResources = (resources: { [key: string]: ApiRes }): void => {
*/


fs.writeFileSync('src/api.ts', lines.join('\n'), { encoding: 'utf-8' })
writeFileSync('src/api.ts', lines.join('\n'), { encoding: 'utf-8' })

console.log('API resources generated.')

Expand Down Expand Up @@ -308,6 +308,8 @@ const generateSpec = (type: string, name: string, resource: Resource): string =>
const lines = spec.split('\n')

const allOperations = ['list', 'create', 'retrieve', 'update', 'delete', 'singleton']

// Generate CRUD operations specs
allOperations.forEach(op => {
if (!Object.values(resource.operations).map(o => {
if ((o.name === 'list') && o.singleton) return 'singleton'
Expand All @@ -321,11 +323,27 @@ const generateSpec = (type: string, name: string, resource: Resource): string =>

spec = lines.join('\n')

// Generate relationships operations specs
Object.keys(resource.operations).filter(o => !allOperations.includes(o)).forEach(o => {
const op = resource.operations[o]
if (op.relationship) {

let specRel = templates.spec_relationship.split('\n').join('\n\t')

specRel = specRel.replace(/##__OPERATION_NAME__##/g, op.name)
specRel = specRel.replace(/##__RELATIONSHIP_TYPE__##/g, op.relationship.type)
console.log(specRel)
spec = spec.replace(/##__RELATIONSHIP_SPECS__##/g, '\n\n\t' + specRel + '\n\t##__RELATIONSHIP_SPECS__##')

}
})

// Header
spec = copyrightHeader(spec)

spec = spec.replace(/##__RESOURCE_CLASS__##/g, name)
spec = spec.replace(/##__RESOURCE_TYPE__##/g, type)
spec = spec.replace(/##__RELATIONSHIP_SPECS__##/g, '')

if (resource.operations.create) {

Expand Down Expand Up @@ -386,8 +404,8 @@ const generateResource = (type: string, name: string, resource: Resource): strin

const resName = name

const types: Set<string> = new Set()
const imports: Set<string> = new Set()
const declaredTypes: Set<string> = new Set()
const declaredImports: Set<string> = new Set()

// Header
res = copyrightHeader(res)
Expand All @@ -400,14 +418,20 @@ const generateResource = (type: string, name: string, resource: Resource): strin
const tpl = op.singleton ? templates['singleton'] : templates[opName]
if (tpl) {
if (['retrieve', 'list'].includes(opName)) {
qryMod.push('QueryParams' + _.capitalize(op.singleton ? 'retrieve' : opName))
qryMod.push('QueryParams' + capitalize(op.singleton ? 'retrieve' : opName))
if ((opName === 'list') && !op.singleton) resMod.push('ListResponse')
}
const tplOp = templatedOperation(resName, opName, op, tpl)
operations.push(tplOp.operation)
tplOp.types.forEach(t => { types.add(t) })
tplOp.types.forEach(t => { declaredTypes.add(t) })
}
else {
if (op.relationship) {
const tplr = templates[`relationship_${op.relationship.cardinality.replace('to_', '')}`]
const tplrOp = templatedOperation('', opName, op, tplr)
operations.push(tplrOp.operation)
} else console.log('Unknown operation: ' + opName)
}
else console.log('Unknown operation: ' + opName)
})
res = res.replace(/##__QUERY_MODELS__##/g, qryMod.join(', '))
res = res.replace(/##__RESPONSE_MODELS__##/g, (resMod.length > 0) ? `, ${resMod.join(', ')}`: '')
Expand All @@ -420,18 +444,19 @@ const generateResource = (type: string, name: string, resource: Resource): strin
if (operations && (operations.length > 0)) res = res.replace(/##__RESOURCE_OPERATIONS__##/g, operations.join('\n\n\t'))

// Interfaces export
const typesArray = Array.from(types)
const typesArray = Array.from(declaredTypes)
res = res.replace(/##__EXPORT_RESOURCE_TYPES__##/g, typesArray.join(', '))

// Interfaces definition
const modIntf: string[] = []
const resIntf: string[] = []
const relTypes: Set<string> = new Set()

typesArray.forEach(t => {
const cudSuffix = getCUDSuffix(t)
resIntf.push(`Resource${cudSuffix}`)
const tplCmp = templatedComponent(resName, t, resource.components[t])
tplCmp.models.forEach(m => imports.add(m))
tplCmp.models.forEach(m => declaredImports.add(m))
modIntf.push(tplCmp.component)
if (cudSuffix) tplCmp.models.forEach(t => relTypes.add(t))
})
Expand All @@ -440,13 +465,13 @@ const generateResource = (type: string, name: string, resource: Resource): strin


// Relationships definition
const relTypesArray = Array.from(relTypes).map(i => `type ${i}Rel = ResourceRel & { type: '${_.snakeCase(Inflector.pluralize(i))}' }`)
const relTypesArray = Array.from(relTypes).map(i => `type ${i}Rel = ResourceRel & { type: '${snakeCase(Inflector.pluralize(i))}' }`)
res = res.replace(/##__RELATIONSHIP_TYPES__##/g, relTypesArray.length ? (relTypesArray.join('\n') + '\n') : '')

// Resources import
const impResMod: string[] = Array.from(imports)
const impResMod: string[] = Array.from(declaredImports)
.filter(i => !typesArray.includes(i)) // exludes resource self reference
.map(i => `import { ${i} } from './${_.snakeCase(Inflector.pluralize(i))}'`)
.map(i => `import { ${i} } from './${snakeCase(Inflector.pluralize(i))}'`)
const importStr = impResMod.join('\n') + (impResMod.length ? '\n' : '')
res = res.replace(/##__IMPORT_RESOURCE_MODELS__##/g, importStr)

Expand All @@ -466,13 +491,18 @@ const templatedOperation = (res: string, name: string, op: Operation, tpl: strin

if (op.requestType) {
const requestType = op.requestType
operation = operation.replace(/##__RESOURCE_REQUEST_TYPE__##/g, requestType)
types.push(requestType)
operation = operation.replace(/##__RESOURCE_REQUEST_CLASS__##/g, requestType)
if (!types.includes(requestType)) types.push(requestType)
}
if (op.responseType || ['list', 'update', 'create'].includes(name)) {
const responseType = op.responseType ? op.responseType : Inflector.singularize(res)
operation = operation.replace(/##__RESOURCE_RESPONSE_TYPE__##/g, responseType)
types.push(responseType)
operation = operation.replace(/##__RESOURCE_RESPONSE_CLASS__##/g, responseType)
if (!types.includes(responseType)) types.push(responseType)
}
if (op.relationship) {
operation = operation.replace(/##__RELATIONSHIP_TYPE__##/g, op.relationship.type)
operation = operation.replace(/##__RELATIONSHIP_PATH__##/g, op.path.substring(1).replace('{', '${'))
operation = operation.replace(/##__RESOURCE_ID__##/g, op.id || 'id')
}

operation = operation.replace(/\n/g, '\n\t')
Expand Down
61 changes: 46 additions & 15 deletions gen/schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */
import fs from 'fs'
import _ from 'lodash'
import { readFileSync, writeFileSync } from 'fs'
import { camelCase, capitalize, snakeCase } from 'lodash'
import axios from 'axios'
import { inspect } from 'util'


// eslint-disable-next-line @typescript-eslint/no-var-requires
const Inflector = require('inflector-js')
Expand All @@ -19,7 +21,7 @@ const downloadSchema = async (url?: string): Promise<string> => {
const response = await axios.get(schemaUrl)
const schema = await response.data

if (schema) fs.writeFileSync(schemaOutPath, JSON.stringify(schema, null, 4))
if (schema) writeFileSync(schemaOutPath, JSON.stringify(schema, null, 4))
else console.log ('OpenAPI schema is empty!')

console.log('OpenAPI schema downloaded: ' + schema.info.version)
Expand All @@ -36,7 +38,7 @@ const parseSchema = (path: string): { version: string, resources: ResourceMap, c
const apiSchema: any = {}

const schemaFile = path
const openApi = fs.readFileSync(schemaFile, { encoding: 'utf-8' }) as any
const openApi = readFileSync(schemaFile, { encoding: 'utf-8' }) as any

const schema = JSON.parse(openApi)

Expand All @@ -54,7 +56,7 @@ const parseSchema = (path: string): { version: string, resources: ResourceMap, c
components: {}
}
Object.keys(apiSchema.components).forEach(c => {
const resName = _.snakeCase(c.replace('Update', '').replace('Create', ''))
const resName = snakeCase(c.replace('Update', '').replace('Create', ''))
if (!c.endsWith('Update') && !c.endsWith('Create')) components[c] = apiSchema.components[c]
if (resName === Inflector.singularize(p)) resources[p].components[c] = apiSchema.components[c]
})
Expand All @@ -68,9 +70,9 @@ const parseSchema = (path: string): { version: string, resources: ResourceMap, c
}


const basicOperationName = (op: string, id?: string): string => {
const operationName = (op: string, id?: string, relationship?: string): string => {
switch (op) {
case 'get': return id ? 'retrieve' : 'list'
case 'get': return id ? (relationship || 'retrieve') : 'list'
case 'patch': return 'update'
case 'delete': return 'delete'
case 'post': return 'create'
Expand All @@ -97,25 +99,29 @@ const parsePaths = (schemaPaths: any[]): PathMap => {
for (const p of Object.entries(schemaPaths)) {

const [pKey, pValue] = p
if (pKey.indexOf('}/') > -1) continue
const relIdx = pKey.indexOf('}/') + 2
const relationship = (relIdx > 1) ? pKey.substring(relIdx) : undefined

const id = pKey.substring(pKey.indexOf('{') + 1, pKey.indexOf('}'))
const path = pKey.replace(/\/{.*}/g, '')
const res = path.substr(1)

const path = pKey.replace(/\/{.*}/g, '').substring(1)
const slIdx = path.lastIndexOf('/')
const res = (slIdx === -1) ? path : path.substring(0, slIdx)

const operations: OperationMap = paths[res] || {}

Object.entries(pValue as object).forEach(o => {

let skip = false

const [oKey, oValue] = o

const singleton = oValue.tags.includes('singleton')

const op: Operation = {
path: pKey,
type: oKey,
name: basicOperationName(oKey, id),
singleton
name: operationName(oKey, id, relationship),
singleton,
}

if (id) op.id = id
Expand All @@ -130,7 +136,31 @@ const parsePaths = (schemaPaths: any[]): PathMap => {
op.responseType = referenceContent(oValue.responses['200'].content)
}

operations[op.name] = op

if (relationship) {

const relCard = oValue.tags[0] as string
if (!relCard) console.log(`Relationship without cardinality: ${op.name} [${op.path}]`)
const relType = oValue.tags[1] as string
if (!relType) console.log(`Relationship without type: ${op.name} [${op.path}]`)
if (!relCard || ! relType) skip = true

if (!skip) {
op.relationship = {
name: relationship || '',
type: relType,
polymorphic: false,
cardinality: (relCard === 'has_many') ? Cardinality.to_many : Cardinality.to_one,
required: false,
deprecated: false
}
op.responseType = Inflector.camelize(Inflector.singularize(op.relationship.type))
}
}


if (skip) console.log(`Operation skipped: ${op.name} [${op.path}]`)
else operations[op.name] = op

})

Expand Down Expand Up @@ -248,8 +278,9 @@ type Operation = {
id?: string
name: string
requestType?: any
responseType?: any,
responseType?: any
singleton: boolean
relationship?: Relationship
}


Expand Down
2 changes: 1 addition & 1 deletion gen/templates/create.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
async create(resource: ##__RESOURCE_REQUEST_TYPE__##, params?: QueryParamsRetrieve, options?: ResourcesConfig): Promise<##__RESOURCE_RESPONSE_TYPE__##> {
async create(resource: ##__RESOURCE_REQUEST_CLASS__##, params?: QueryParamsRetrieve, options?: ResourcesConfig): Promise<##__RESOURCE_RESPONSE_CLASS__##> {
return this.resources.create({ ...resource, type: ##__RESOURCE_CLASS__##.TYPE }, params, options)
}
2 changes: 1 addition & 1 deletion gen/templates/delete.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
async delete(id: string, options?: ResourcesConfig): Promise<void> {
await this.resources.delete({ type: ##__RESOURCE_CLASS__##.TYPE, id }, options)
}
}
Loading

0 comments on commit 15240c9

Please sign in to comment.