Skip to content

Commit

Permalink
feat: add auto generation of special actions
Browse files Browse the repository at this point in the history
  • Loading branch information
pviti committed Dec 13, 2023
1 parent 75f5f97 commit 08cd9ff
Show file tree
Hide file tree
Showing 13 changed files with 736 additions and 561 deletions.
38 changes: 32 additions & 6 deletions gen/generator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import apiSchema, { Resource, Operation, Component, Cardinality, Attribute } from './schema'
import apiSchema, { Resource, Operation, Component, Cardinality, Attribute, isObjectType } from './schema'
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync } from 'fs'
import { basename } from 'path'
import { snakeCase } from 'lodash'
Expand All @@ -9,7 +9,8 @@ import fixSchema from './fixer'
/**** SDK source code generator settings ****/
const CONFIG = {
RELATIONSHIP_FUNCTIONS: true,
TRIGGER_FUNCTIONS: true
TRIGGER_FUNCTIONS: true,
ACTION_FUNCTIONS: true
}
/**** **** **** **** **** **** **** **** ****/

Expand Down Expand Up @@ -536,6 +537,7 @@ const generateResource = (type: string, name: string, resource: Resource): strin
let resModelType = 'ApiResource'

const declaredTypes: Set<string> = new Set([resModelInterface])
const declaredTypesDef: string[] = []
// const declaredEnums: ComponentEnums = {}
const declaredImportsModels: Set<string> = new Set()
const declaredImportsCommon: Set<string> = new Set(['ResourceId'])
Expand Down Expand Up @@ -576,7 +578,15 @@ const generateResource = (type: string, name: string, resource: Resource): strin
resMod.add('ListResponse')
}
operations.push(tplrOp.operation)
} else console.log('Unknown operation: ' + opName)
}
else
if (op.action && CONFIG.ACTION_FUNCTIONS) {
const tpla = templates['action']
const tplaOp = templatedOperation(resName, opName, op, tpla)
operations.push(tplaOp.operation)
tplaOp.typesDef.forEach(t => { declaredTypesDef.push(t) })
}
else console.log('Unknown operation: ' + opName)
}
})

Expand Down Expand Up @@ -607,6 +617,8 @@ const generateResource = (type: string, name: string, resource: Resource): strin
// Interfaces export
const typesArray = Array.from(declaredTypes)
res = res.replace(/##__EXPORT_RESOURCE_TYPES__##/g, typesArray.join(', '))
const typesDefArray = Array.from(declaredTypesDef)
res = res.replace(/##__EXPORT_RESOURCE_TYPES_DEF__##/g, (typesDefArray.length > 0)? `${typesDefArray.join('\n')}\n` : '')

// Interfaces and types definition
const modelInterfaces: string[] = []
Expand Down Expand Up @@ -647,18 +659,23 @@ const generateResource = (type: string, name: string, resource: Resource): strin
}


const templatedOperation = (res: string, name: string, op: Operation, tpl: string, placeholders?: Record<string, string>): { operation: string, types: string[] } => {
const templatedOperation = (res: string, name: string, op: Operation, tpl: string, placeholders?: Record<string, string>): { operation: string, types: string[], typesDef: string[] } => {

let operation = tpl
const types: string[] = []
const typesDef: string[] = []

operation = operation.replace(/##__OPERATION_NAME__##/g, name)
operation = operation.replace(/##__RESOURCE_CLASS__##/g, res)

if (op.requestType) {
const requestType = op.requestType
operation = operation.replace(/##__RESOURCE_REQUEST_CLASS__##/g, requestType)
if (!types.includes(requestType)) types.push(requestType)
if (isObjectType(requestType)) {
const typeDef = `export type ${Inflector.camelize(op.name)}DataType = { ${Object.entries(op.requestTypeDef).map(([k, v]: [string, any]) => `${k}${v.nullable? '?' : ''}: ${v.type}`).join(', ')} }`
typesDef.push(typeDef)
}
else if (!types.includes(requestType)) types.push(requestType)
}
if (op.responseType) {
const responseType = op.responseType
Expand All @@ -680,6 +697,15 @@ const templatedOperation = (res: string, name: string, op: Operation, tpl: strin
operation = operation.replace(/##__TRIGGER_VALUE__##/, placeholders?.trigger_value? ` triggerValue: ${ placeholders.trigger_value},` : '')
operation = operation.replace(/##__TRIGGER_VALUE_TYPE__##/, placeholders?.trigger_value? 'triggerValue' : 'true')
}
else
if (op.action) { // Action
operation = operation.replace(/##__ACTION_PATH__##/g, op.path.substring(1).replace('{' + op.id, '${_' + opIdVar))
operation = operation.replace(/##__RESOURCE_ID__##/g, opIdVar)
operation = operation.replace(/##__MODEL_RESOURCE_INTERFACE__##/g, Inflector.singularize(res))
operation = operation.replace(/##__ACTION_PAYLOAD_PARAM__##/g, isObjectType(op.requestType)? ` payload: ${Inflector.camelize(op.name)}DataType,` : '')
operation = operation.replace(/##__ACTION_PAYLOAD__##/g, isObjectType(op.requestType)? ' ...payload ' : '')
operation = operation.replace(/##__ACTION_COMMAND__##/g, op.type)
}

if (placeholders) Object.entries(placeholders).forEach(([key, val]) => {
const plh = (key.startsWith('##__') && key.endsWith('__##'))? key : `##__${key.toUpperCase()}__##`
Expand All @@ -689,7 +715,7 @@ const templatedOperation = (res: string, name: string, op: Operation, tpl: strin
operation = operation.replace(/\n/g, '\n\t')


return { operation, types }
return { operation, types, typesDef }

}

Expand Down
1 change: 1 addition & 0 deletions gen/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,7 @@
"summary": "Related organization",
"description": "Related organization",
"tags": [
"api_credentials",
"has_many"
],
"parameters": [
Expand Down
98 changes: 57 additions & 41 deletions gen/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ const parseSchema = (path: string): ApiSchema => {
}


const operationName = (op: string, id?: string, relationship?: string): string => {
const operationName = (op: string, id?: string, relOrCmd?: string): string => {
switch (op) {
case 'get': return id ? (relationship || 'retrieve') : 'list'
case 'patch': return 'update'
case 'get': return id ? (relOrCmd || 'retrieve') : 'list'
case 'patch': return id ? (relOrCmd || 'update') : 'update'
case 'delete': return 'delete'
case 'post': return 'create'
case 'post': return id ? (relOrCmd || 'create') : 'create'
default: return op
}
}
Expand All @@ -121,27 +121,29 @@ const referenceResource = (ref: { '$ref': string }): string | undefined => {
return r? Inflector.camelize(r.substring(r.lastIndexOf('/') + 1)) : undefined
}

const contentSchema = (content: any): any => {
return content["application/vnd.api+json"]?.schema
}

const referenceContent = (content: any): string | undefined => {
// No content or no response codes
if (!content || ! Object.keys(content).length) return undefined
const schema = content["application/vnd.api+json"]?.schema
const schema = contentSchema(content)
return schema? referenceResource(schema) : undefined
}

/*
const checkSingletonTags = (tags: string[]): boolean => {
console.log(tags)
let singleton = false
if (tags.includes('singleton')) singleton = true
else {
const type = tags.find(t => !t.startsWith('has_'))
singleton = type? (type === Inflector.singularize(type)) : false
}
console.log(singleton)
return singleton

const contentType = (content?: any): string | undefined => {
if (!content) return undefined
const schema = contentSchema(content)
if (!schema) return undefined
return isReference(schema)? referenceContent(content) : 'object'
}


export const isObjectType = (type?: string): boolean => {
return (type !== undefined) && ['object', 'any'].includes(type)
}
*/


const parsePaths = (schemaPaths: any[]): PathMap => {
Expand All @@ -152,7 +154,7 @@ const parsePaths = (schemaPaths: any[]): PathMap => {

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

const id = pKey.substring(pKey.indexOf('{') + 1, pKey.indexOf('}'))
const path = pKey.replace(/\/{.*}/g, '').substring(1)
Expand All @@ -167,45 +169,50 @@ const parsePaths = (schemaPaths: any[]): PathMap => {

const [oKey, oValue] = o

const singleton = /* checkSingletonTags(oValue.tags) */ oValue.tags.includes('singleton')

const op: Operation = {
path: pKey,
type: oKey,
name: operationName(oKey, id, relationship),
singleton,
name: operationName(oKey, id, relOrCmd),
singleton: oValue.tags.includes('singleton'),
}

if (id) op.id = id
if (oValue.requestBody) op.requestType = referenceContent(oValue.requestBody.content)
if (oValue.requestBody) {
op.requestType = contentType(oValue.requestBody.content)
if (isObjectType(op.requestType)) op.requestTypeDef = contentSchema(oValue.requestBody.content).properties.data.properties.attributes.properties
}
if (oValue.responses) {
const responses = Object.values(oValue.responses) as any[]
if (responses.length > 0) op.responseType = referenceContent(responses[0].content)
if (responses.length > 0) op.responseType = contentType(responses[0].content)
}


if (relationship) {
if (relOrCmd) {

const tags = oValue.tags as string[]
const relCard = tags.find(t => t.startsWith('has_')) as string
if (!relCard) console.log(`Relationship without cardinality: ${op.name} [${op.path}]`)
const relType = tags.find(t => !t.startsWith('has_')) 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

if (relType) {
if (relCard) {
op.relationship = {
name: relOrCmd || '',
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))
} else {
op.function = operationName(oKey, id,),
op.action = true
}
op.responseType = Inflector.camelize(Inflector.singularize(op.relationship.type))
}
}
} else skip = true

}

if (skip) console.log(`Operation skipped: ${op.name} [${op.path}]`)
else operations[op.name] = op
Expand All @@ -222,6 +229,11 @@ const parsePaths = (schemaPaths: any[]): PathMap => {
}


const isReference = (obj: any): boolean => {
return (obj.$ref !== undefined)
}


const getReference = (obj: any): string | undefined => {
if (obj) return obj['$ref']
return undefined
Expand Down Expand Up @@ -373,11 +385,15 @@ type Operation = {
type: string
id?: string
name: string
requestType?: any
responseType?: any
function?: string
requestType?: string
requestTypeDef?: any
responseType?: string
responseTypeDef?: any
singleton: boolean
relationship?: Relationship
trigger?: boolean
trigger?: boolean,
action?: boolean
}


Expand Down
4 changes: 4 additions & 0 deletions gen/templates/action.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
async ##__OPERATION_NAME__##(##__RESOURCE_ID__##: string | ##__MODEL_RESOURCE_INTERFACE__##,##__ACTION_PAYLOAD_PARAM__## options?: ResourcesConfig): Promise<void> {
const _##__RESOURCE_ID__## = (##__RESOURCE_ID__## as ##__MODEL_RESOURCE_INTERFACE__##).id || ##__RESOURCE_ID__## as string
await this.resources.action('##__ACTION_COMMAND__##', `##__ACTION_PATH__##`, {##__ACTION_PAYLOAD__##}, options)
}
1 change: 1 addition & 0 deletions gen/templates/resource.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ class ##__RESOURCE_CLASS__## extends ##__RESOURCE_MODEL_TYPE__##<##__MODEL_RESOU
export default ##__RESOURCE_CLASS__##

export type { ##__EXPORT_RESOURCE_TYPES__##, ##__MODEL_RESOURCE_INTERFACE__##Type }
##__EXPORT_RESOURCE_TYPES_DEF__##
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"axios": "1.5.1"
},
"devDependencies": {
"@babel/preset-env": "^7.23.5",
"@babel/preset-env": "^7.23.6",
"@babel/preset-typescript": "^7.23.3",
"@commercelayer/eslint-config-ts": "1.1.0",
"@commercelayer/js-auth": "^4.2.0",
Expand All @@ -54,8 +54,8 @@
"jsonapi-typescript": "^0.1.3",
"lodash": "^4.17.21",
"minimize-js": "^1.4.0",
"semantic-release": "^22.0.10",
"ts-node": "^10.9.1",
"semantic-release": "^22.0.12",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"repository": "github:commercelayer/provisioning-sdk",
Expand Down
Loading

0 comments on commit 08cd9ff

Please sign in to comment.