Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions cli/src/codegen/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
interface FieldSchema {
name: string;
type: string;
target?: string;
precision?: string;
}

interface ModelSchema {
name: string;
label: string;
collection: string;
fields: FieldSchema[];
}

const HEADER = '// AUTO-GENERATED by `everywhere bind` -- do not edit manually.\n';

function fieldTypeToTS(field: FieldSchema): string {
switch (field.type) {
case 'TEXT':
case 'DATE':
case 'SINGLE_INSTANCE':
return 'string';
case 'BOOLEAN':
return 'boolean';
case 'MULTI_INSTANCE':
return 'string[]';
default:
return 'unknown';
}
}

function pluralize(name: string): string {
if (name.endsWith('s')) return name + 'es';
if (name.endsWith('y') && !/[aeiou]y$/i.test(name)) return name.slice(0, -1) + 'ies';
return name + 's';
}

export function generateModels(schemas: ModelSchema[]): string {
const lines = [HEADER];

for (const schema of schemas) {
lines.push(`export interface ${schema.name} {`);
lines.push(' id: string;');
for (const field of schema.fields) {
lines.push(` ${field.name}: ${fieldTypeToTS(field)};`);
}
lines.push('}');
lines.push('');
}

return lines.join('\n');
}

export function generateSchema(schemas: ModelSchema[]): string {
const lines = [HEADER];

lines.push("import type { ModelSchema } from '@workday/everywhere';");
lines.push('');
lines.push('export const schemas: Record<string, ModelSchema> = {');

for (const schema of schemas) {
lines.push(` ${schema.name}: {`);
lines.push(` name: '${schema.name}',`);
lines.push(` label: '${schema.label}',`);
lines.push(` collection: '${schema.collection}',`);
lines.push(' fields: [');
for (const field of schema.fields) {
const parts = [`name: '${field.name}'`, `type: '${field.type}'`];
if (field.target) parts.push(`target: '${field.target}'`);
if (field.precision) parts.push(`precision: '${field.precision}'`);
lines.push(` { ${parts.join(', ')} },`);
}
lines.push(' ],');
lines.push(' },');
}

lines.push('};');
lines.push('');
return lines.join('\n');
}

export function generateModelHooks(schema: ModelSchema): string {
const { name } = schema;
const plural = pluralize(name);
const lines = [HEADER];

lines.push("import { useQuery, useMutation } from '@workday/everywhere';");
lines.push(`import type { ${name} } from './models.js';`);
lines.push('');
lines.push(`export function use${plural}(options?: Parameters<typeof useQuery>[1]) {`);
lines.push(` return useQuery<${name}>('${name}', options);`);
lines.push('}');
lines.push('');
lines.push(`export function use${name}(id: string) {`);
lines.push(` return useQuery<${name}>('${name}', { id });`);
lines.push('}');
lines.push('');
lines.push(`export function use${name}Mutation() {`);
lines.push(` return useMutation<${name}>('${name}');`);
lines.push('}');
lines.push('');

return lines.join('\n');
}

export function generateIndex(schemas: ModelSchema[]): string {
const lines = [HEADER];

lines.push("export * from './models.js';");
lines.push("export * from './schema.js';");
for (const schema of schemas) {
lines.push(`export * from './${schema.name}.js';`);
}
lines.push('');

return lines.join('\n');
}
2 changes: 2 additions & 0 deletions cli/src/codegen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { parseBusinessObject } from './parser.js';
export { generateModels, generateSchema, generateModelHooks, generateIndex } from './generator.js';
53 changes: 53 additions & 0 deletions cli/src/codegen/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
interface FieldSchema {
name: string;
type: string;
target?: string;
precision?: string;
}

interface ModelSchema {
name: string;
label: string;
collection: string;
fields: FieldSchema[];
}

interface BusinessObjectField {
name: string;
type: string;
target?: string;
precision?: string;
}

interface BusinessObjectDefinition {
name: string;
label: string;
defaultCollection: { name: string };
fields: BusinessObjectField[];
}

export function parseBusinessObject(definition: BusinessObjectDefinition): ModelSchema {
return {
name: definition.name,
label: definition.label,
collection: definition.defaultCollection.name,
fields: definition.fields.map(parseField),
};
}

function parseField(field: BusinessObjectField): FieldSchema {
const schema: FieldSchema = {
name: field.name,
type: field.type as FieldSchema['type'],
};

if (field.target) {
schema.target = field.target;
}

if (field.precision) {
schema.precision = field.precision;
}

return schema;
}
90 changes: 90 additions & 0 deletions cli/src/commands/everywhere/bind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Args } from '@oclif/core';
import * as fs from 'node:fs';
import * as path from 'node:path';

import EverywhereBaseCommand from './base';
import { parseBusinessObject } from '../../codegen/parser';
import {
generateModels,
generateSchema,
generateModelHooks,
generateIndex,
} from '../../codegen/generator';

const CONFIG_DIR = 'everywhere';
const CONFIG_FILE = '.config.json';
const OUTPUT_DIR = 'data';

export default class BindCommand extends EverywhereBaseCommand {
static description =
'Generate TypeScript types and data hooks from Workday Extend business object models.';

static args = {
'app-dir': Args.directory({
description:
'Directory containing model/ subfolder with .businessobject files. Saved for future runs.',
required: false,
}),
};

static flags = {
...EverywhereBaseCommand.baseFlags,
};

async run(): Promise<void> {
const { args } = await this.parse(BindCommand);
const pluginDir = await this.parsePluginDir();
const everywhereDir = path.join(pluginDir, CONFIG_DIR);
const configPath = path.join(everywhereDir, CONFIG_FILE);

// Resolve the app directory
let appDir: string;

if (args['app-dir']) {
appDir = path.resolve(args['app-dir']);
// Save to config for future runs
fs.mkdirSync(everywhereDir, { recursive: true });
fs.writeFileSync(configPath, JSON.stringify({ extend: appDir }, null, 2) + '\n');
} else if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
appDir = path.resolve(pluginDir, config.extend);
} else {
// Fall back to plugin dir itself (model/ in the plugin)
appDir = pluginDir;
}

// Find .businessobject files
const modelDir = path.join(appDir, 'model');
if (!fs.existsSync(modelDir)) {
this.error(`No model/ directory found in ${appDir}`);
}

const files = fs.readdirSync(modelDir).filter((f) => f.endsWith('.businessobject'));
if (files.length === 0) {
this.error(`No .businessobject files found in ${modelDir}`);
}

// Parse all business objects
const schemas = files.map((file) => {
const content = fs.readFileSync(path.join(modelDir, file), 'utf-8');
return parseBusinessObject(JSON.parse(content));
});

// Generate output
const outputDir = path.join(everywhereDir, OUTPUT_DIR);
fs.mkdirSync(outputDir, { recursive: true });

fs.writeFileSync(path.join(outputDir, 'models.ts'), generateModels(schemas));
fs.writeFileSync(path.join(outputDir, 'schema.ts'), generateSchema(schemas));
fs.writeFileSync(path.join(outputDir, 'index.ts'), generateIndex(schemas));

for (const schema of schemas) {
fs.writeFileSync(path.join(outputDir, `${schema.name}.ts`), generateModelHooks(schema));
}

this.log(
`Generated types for ${schemas.length} model(s): ${schemas.map((s) => s.name).join(', ')}`
);
this.log(`Output: ${outputDir}`);
}
}
Loading
Loading