Skip to content

Commit

Permalink
work on codegen.ts
Browse files Browse the repository at this point in the history
  • Loading branch information
narthur committed Apr 9, 2024
1 parent 50865f4 commit 4a531a1
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 57 deletions.
97 changes: 78 additions & 19 deletions src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import z from "zod";
import fs from "fs";
import makeType from "./codegen/makeType.js";
import makeClassMethods from "./codegen/makeClassMethods.js";
import path from "path";

export default async function main(): Promise<void> {
const raw = rc("baserow");
Expand All @@ -12,41 +13,99 @@ export default async function main(): Promise<void> {
url: z.string(),
tables: z.record(z.string(), z.number()),
databaseToken: z.string(),
outDir: z.string(),
config: z.string(),
})
.parse(raw);

console.log("Hello from codegen.ts");
console.dir(config);

if (!config.databaseToken) {
throw new Error("Missing databaseToken in .baserowrc");
}
const outDir = path.join(path.dirname(config.config), config.outDir);

if (!fs.existsSync("./__generated__")) {
fs.mkdirSync("./__generated__");
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}

const sdk = new BaserowSdk(String(config.databaseToken));

await Promise.all(
Object.entries(config.tables).map(async ([tableName, tableId]) => {
const fields = await sdk.listFields(tableId);
console.log(tableName);
console.log(tableId);
console.log(fields);
const tables = await Promise.all(
Object.entries(config.tables).map(async ([name, id]) => {
return {
name,
id,
fields: await sdk.listFields(id),
};
}),
);

tables.map((table) => {
console.log(table);
const tableName = table.name;
const fields = table.fields;
const foreignTables = fields
.filter((field) => !!field.link_row_table_id)
.map((field) =>
tables.find((table) => table.id === field.link_row_table_id),
)
.filter((t) => !!t);

//TODO: this may not be correct for all generated files
const typeDef = `export type ${tableName}RowType = ${makeType(fields)}
//TODO: this may not be correct for all generated files
const typeDef = `export type ${tableName}RowType = ${makeType(fields)}
import { Row } from "../src/row.ts";
import { Repository } from "./Repository.ts";
import { BaserowSdk } from "../src/index.ts";
${foreignTables
.map((t) => {
return `import { ${t?.name}Row } from "./${t?.name}.ts";`;
})
.join("\n")}
import { Base } from "../src/base.ts";
export class ${tableName}Row extends Row<${tableName}RowType> {
private repository: Repository;
constructor(options: {
tableId: number;
rowId: number;
row: ${tableName}RowType;
sdk: BaserowSdk;
repository: Repository;
}) {
super(options);
this.repository = options.repository;
}
${makeClassMethods(table.id, tables)}
}`;

fs.writeFileSync(`${outDir}/${tableName}.ts`, typeDef);
});

export class ${tableName}Row extends Base<${tableName}RowType> {
${makeClassMethods(fields)}
const factoryCode = `import { Factory } from '../src/factory.ts'
import { ListRowsOptions, GetRowOptions } from '../src/index.ts'
${Object.keys(config.tables)
.map(
(tableName) =>
`import { ${tableName}Row, ${tableName}RowType } from './${tableName}.ts';`,
)
.join("\n")}
export class Repository extends Factory {
${Object.keys(config.tables)
.map(
(tableName) =>
`public async getMany${tableName}(options: ListRowsOptions = {}): Promise<${tableName}Row[]> {
const {results} = await this.sdk.listRows<${tableName}RowType>(${config.tables[tableName]}, options);
return results.map((row) => new ${tableName}Row({tableId: ${config.tables[tableName]}, rowId: row.id, row, sdk: this.sdk, repository: this}));
}
public async getOne${tableName}(id: number, options: GetRowOptions = {}): Promise<${tableName}Row> {
const row = await this.sdk.getRow<${tableName}RowType>(${config.tables[tableName]}, id, options);
return new ${tableName}Row({tableId: ${config.tables[tableName]}, rowId: row.id, row, sdk: this.sdk, repository: this});
}
`,
)
.join("\n")}
}`;

fs.writeFileSync(`./__generated__/${tableName}.ts`, typeDef);
}),
);
fs.writeFileSync(`${outDir}/Repository.ts`, factoryCode);

// console.dir(tables);
}
67 changes: 50 additions & 17 deletions src/codegen/makeClassMethods.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { describe, it, expect } from "vitest";
import makeClassMethods from "./makeClassMethods";
import f from "../test/fixtures/fieldDefinition";
import { ListFieldsResponse } from "../index.js";

function run(fields: ListFieldsResponse = []): string {
return makeClassMethods(1, [{ id: 1, name: "the_table_name", fields }]);
}

describe("makeClassMethods", () => {
it("returns empty string for empty fields", () => {
expect(makeClassMethods([])).toBe("");
expect(run()).toBe("");
});

it("returns string for single field", () => {
expect(
makeClassMethods([
run([
f({
name: "the_field_name",
}),
Expand All @@ -19,7 +24,7 @@ describe("makeClassMethods", () => {

it("returns string for multiple fields", () => {
expect(
makeClassMethods([
run([
f({
name: "the_field_name",
}),
Expand All @@ -32,7 +37,7 @@ describe("makeClassMethods", () => {

it("handles field names with spaces", () => {
expect(
makeClassMethods([
run([
f({
name: "the field name",
}),
Expand All @@ -42,7 +47,7 @@ describe("makeClassMethods", () => {

it("handles field names with spaces in setter", () => {
expect(
makeClassMethods([
run([
f({
name: "the field name",
}),
Expand All @@ -52,7 +57,7 @@ describe("makeClassMethods", () => {

it("sets return type", () => {
expect(
makeClassMethods([
run([
f({
name: "the_field_name",
type: "text",
Expand All @@ -63,7 +68,7 @@ describe("makeClassMethods", () => {

it("uses mapped type for value arg", () => {
expect(
makeClassMethods([
run([
f({
name: "the_field_name",
type: "text",
Expand All @@ -74,7 +79,7 @@ describe("makeClassMethods", () => {

it("uses mapped type for getField generic", () => {
expect(
makeClassMethods([
run([
f({
name: "the_field_name",
type: "text",
Expand All @@ -85,7 +90,7 @@ describe("makeClassMethods", () => {

it("handles emoji field name", () => {
expect(
makeClassMethods([
run([
f({
name: "🔥",
}),
Expand All @@ -95,7 +100,7 @@ describe("makeClassMethods", () => {

it("handles lookup types", () => {
expect(
makeClassMethods([
run([
f({
name: "the_field_name",
type: "lookup",
Expand All @@ -107,7 +112,7 @@ describe("makeClassMethods", () => {

it("parses numbers", () => {
expect(
makeClassMethods([
run([
f({
name: "the_field_name",
type: "number",
Expand All @@ -118,7 +123,7 @@ describe("makeClassMethods", () => {

it("does not create setter for read-only field", () => {
expect(
makeClassMethods([
run([
f({
name: "the_field_name",
read_only: true,
Expand All @@ -129,12 +134,40 @@ describe("makeClassMethods", () => {

it("accepts id array for link_row setters", () => {
expect(
makeClassMethods([
f({
name: "the_field_name",
type: "link_row",
}),
makeClassMethods(1, [
{
id: 1,
name: "the_table_name",
fields: [f({ type: "link_row", link_row_table_id: 2 })],
},
{ id: 2, name: "the_foreign_table_name", fields: [] },
]),
).toContain("value: number[]");
});

it("uses repository to get linked row objects", () => {
expect(
makeClassMethods(1, [
{
id: 1,
name: "the_table_name",
fields: [f({ type: "link_row", link_row_table_id: 2 })],
},
{ id: 2, name: "the_foreign_table_name", fields: [] },
]),
).toContain("this.repository.getOneTheForeignTableName");
});

it("properly set link row getter return type", () => {
expect(
makeClassMethods(1, [
{
id: 1,
name: "the_table_name",
fields: [f({ type: "link_row", link_row_table_id: 2 })],
},
{ id: 2, name: "the_foreign_table_name", fields: [] },
]),
).toContain("TheForeignTableNameRow[]");
});
});
44 changes: 26 additions & 18 deletions src/codegen/makeClassMethods.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import { FieldDefinition, ListFieldsResponse } from "../index.js";
import { makeFieldType } from "./makeFieldType.js";
import * as emoji from "node-emoji";

function toCamelCase(str: string): string {
return emoji
.unemojify(str)
.replaceAll(":", "")
.replace(/([-_\s:][a-z])/gi, ($1) => {
return $1
.toUpperCase()
.replace("-", "")
.replace("_", "")
.replace(" ", "");
});
}
import { toCamelCase } from "./toCamelCase.js";

function makeGetter(field: FieldDefinition): string {
function makeGetter(
field: FieldDefinition,
tables: { id: number; name: string; fields: ListFieldsResponse }[],
): string {
const t = makeFieldType(field);

if (t.includes("number | string")) {
return `public ${toCamelCase(`get ${field.name}`)}(): number {
return parseFloat(this.getField<${t}>("${field.name}").toString());
}`;
}
if (field.type === "link_row") {
if (!field.link_row_table_id) {
throw new Error("link_row_table_id is missing");
}
const foreignTable = tables.find((t) => field.link_row_table_id === t.id);
if (!foreignTable) {
throw new Error("foreign table not found");
}
return `public ${toCamelCase(`get ${field.name}`)}(): Promise<${toCamelCase(foreignTable.name, true)}Row[]> {
return Promise.all(this.getField<{ "id": number, "value": string }[]>("${field.name}").map((r) => {
return this.repository.${toCamelCase(`get one ${foreignTable.name}`)}(r.id);
}));
}`;
}

return `public ${toCamelCase(`get ${field.name}`)}(): ${t} {
return this.getField<${t}>("${field.name}");
Expand All @@ -43,11 +47,15 @@ function makeSetter(field: FieldDefinition): string {
}`;
}

export default function makeClassMethods(fields: ListFieldsResponse): string {
export default function makeClassMethods(
tableId: number,
tables: { id: number; name: string; fields: ListFieldsResponse }[],
): string {
const methods: string[] = [];
const table = tables.find((t) => t.id === tableId);

fields.forEach((field) => {
methods.push(makeGetter(field));
table?.fields.forEach((field) => {
methods.push(makeGetter(field, tables));
if (!field.read_only) {
methods.push(makeSetter(field));
}
Expand Down
20 changes: 20 additions & 0 deletions src/codegen/toCamelCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as emoji from "node-emoji";

export function toCamelCase(str: string, upper: boolean = false): string {
const _str = emoji
.unemojify(str)
.replaceAll(":", "")
.replace(/([-_\s:][a-z])/gi, ($1) => {
return $1
.toUpperCase()
.replace("-", "")
.replace("_", "")
.replace(" ", "");
});

if (upper) {
return _str.charAt(0).toUpperCase() + _str.slice(1);
}

return _str;
}
8 changes: 8 additions & 0 deletions src/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { BaserowSdk } from "./index.js";

export abstract class Factory {
protected sdk: BaserowSdk;
constructor({ sdk }: { sdk: BaserowSdk }) {
this.sdk = sdk;
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type FieldDefinition = {
read_only: boolean;
array_formula_type?: string;
formula_type?: string;
link_row_table_id?: number;
};

export type ListFieldsResponse = Array<FieldDefinition>;
Expand Down
Loading

0 comments on commit 4a531a1

Please sign in to comment.