diff --git a/gen/.gitignore b/gen/.gitignore new file mode 100644 index 0000000..ab5afb2 --- /dev/null +++ b/gen/.gitignore @@ -0,0 +1,176 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + diff --git a/gen/README.md b/gen/README.md new file mode 100644 index 0000000..3e92546 --- /dev/null +++ b/gen/README.md @@ -0,0 +1,15 @@ +# Code generator + + +## CI YAMl + +```bash +bun run gen/src/genCI.ts +``` + + +### Install deps + +```bash +cd gen && bun i +``` \ No newline at end of file diff --git a/gen/bun.lockb b/gen/bun.lockb new file mode 100755 index 0000000..9e972f2 Binary files /dev/null and b/gen/bun.lockb differ diff --git a/gen/package.json b/gen/package.json new file mode 100644 index 0000000..9d820b8 --- /dev/null +++ b/gen/package.json @@ -0,0 +1,16 @@ +{ + "name": "gen", + "module": "index.ts", + "type": "module", + "devDependencies": { + "bun-types": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "js-yaml": "^4.1.0", + "ts-dedent": "^2.2.0", + "zod": "^3.22.4" + } +} \ No newline at end of file diff --git a/gen/src/GenBase.ts b/gen/src/GenBase.ts new file mode 100644 index 0000000..abb3f71 --- /dev/null +++ b/gen/src/GenBase.ts @@ -0,0 +1,79 @@ +/** + * Generator base class + * - provides basic functions to help with code generation + */ + +type FileType = "ex" | "ts"; +export class GenBase { + logLevel = 1; // change for more verbose logging + lines: string[] = []; + indentLevel = 0; + prefix = ""; + generatorName = ""; + + get content() { + return this.lines.join("\n"); + } + + push(line: string) { + this.lines.push(this.prefix + line); + } + plainPush(line: string) { + this.lines.push(line); + } + indentUp(amount = 1) { + this.indentLevel += amount; + this.setPrefix(); + } + + indentDown(amount = 1) { + this.indentLevel -= amount; + this.setPrefix(); + } + + setPrefix() { + this.prefix = Array(this.indentLevel).fill(" ").join(""); + } + + withIndent(func: () => void) { + this.indentUp(); + func(); + this.indentDown(); + } + + addBanner(type: FileType) { + if (type === "ts") { + this.push( + `// **** GENERATED CODE! see ${this.generatorName} for details. ****` + ); + } + if (type === "ex") { + this.push( + `## **** GENERATED CODE! see ${this.generatorName} for details. ****` + ); + } + this.push(""); + } + + logRun() { + this.log(`*** RUN GENERATOR ${this.generatorName}`); + } + + log(...s: unknown[]) { + console.log(...s); + } + + warn(...s: unknown[]) { + if (this.logLevel < 2) { + return; + } + this.log(...s); + } + + debug(...s: unknown[]) { + if (this.logLevel < 3) { + return; + } + this.log(...s); + } +} \ No newline at end of file diff --git a/gen/src/RepoGenerator.ts b/gen/src/RepoGenerator.ts new file mode 100644 index 0000000..fbc449c --- /dev/null +++ b/gen/src/RepoGenerator.ts @@ -0,0 +1,374 @@ +import path from "node:path" +import { GenBase } from "./GenBase"; +import { lpad } from "./utils"; +import dedent from "ts-dedent"; + +export class RepoGenerator extends GenBase { + constructor() { + super(); + this.generatorName = "gen/src/RepoGenerator.ts"; + } + + run() { + this.logRun(); + this.addBanner("ex"); + this.push("defmodule Surreal.Repo do"); + this.withIndent(() => { + this.genFrontStatic(); + this.genMethods(); + this.genQueryMethod(); + this.genAdminHelpers(); + }); + this.push("end"); + this.push(""); + } + + methodsCreate() { + return ["create", "insert"] + } + methodsWithIdData() { + return ["update", "merge", "patch"] + } + methodsWithId() { + return ["select", "delete"] + } + + genMethods() { + this.methodsCreate().map(method => { + this.genMethodCreation(method) + }) + this.methodsWithIdData().map(method => { + this.genMethodWithData(method) + }) + this.methodsWithId().map(method => { + this.genMethodWithId(method) + }) + + } + genMethodWithData(name: string) { + let methods = dedent` + + ${this.methodBanner(name)} + + ${this.variationChangesetCreation(name) + "\n"} + ${this.variationModuleIdData(name) + "\n"} + ${this.variationModuleData(name) + "\n"} + ${this.variationBinaryThingData(name) + "\n"} + ` + methods = lpad(methods, " ") + this.push(methods) + } + + genMethodWithId(name: string) { + let methods = dedent` + + ${this.methodBanner(name)} + + ${this.variationModuleId(name) + "\n"} + ${this.variationModule(name) + "\n"} + ${this.variationBinaryThing(name) + "\n"} + ` + methods = lpad(methods, " ") + this.push(methods) + } + + genMethodCreation(name: string) { + let methods = dedent` + + ${this.methodBanner(name)} + + ${this.variationChangesetCreation(name) + "\n"} + ${this.variationSchemaCreation(name) + "\n"} + ${this.variationModuleIdData(name) + "\n"} + ${this.variationModuleDataCreation(name) + "\n"} + ${this.variationBinaryThingData(name) + "\n"} + ` + methods = lpad(methods, " ") + this.push(methods) + } + + methodBanner(name: string) { + return dedent` + + ### + ### ${name.toUpperCase()} ######### + ### + ` + } + + variationChangesetCreation(name: string) { + return dedent` + # with changeset + def ${name}(%Ecto.Changeset{} = changeset) do + {module, data} = module_data_from_changeset(changeset) + ${name}(module, data) + end + # with list of changesets + def ${name}(changesets) when is_list(changesets) do + {module, data} = module_data_from_changeset(changesets) + ${name}(module, data) + end + ` + } + variationSchemaCreation(name: string) { + return dedent` + # with ecto schema + def ${name}(struct) when is_struct(struct) do + ${name}(struct_to_changeset(struct)) + end + ` + } + variationBinaryThing(name: string) { + return dedent` + # with binary id + def ${name}(thing) when is_binary(thing) do + Surrealix.${name}(current_repo(), thing) + end + ` + } + variationBinaryThingData(name: string) { + return dedent` + # with binary id / data (map) + def ${name}(thing, data) when is_binary(thing) and is_map(data) do + Surrealix.${name}(current_repo(), thing, data) + end + # with binary id / data (list) + def ${name}(thing, data) when is_binary(thing) and is_list(data) do + Surrealix.${name}(current_repo(), thing, data) + end + ` + } + variationModule(name: string) { + return dedent` + # with module + def ${name}(module) when is_atom(module) do + ${name}(to_thing(module)) |> as(module) + end + ` + } + variationModuleId(name: string) { + return dedent` + # with module / id + def ${name}(module, id) when is_atom(module) do + ${name}(to_thing(module, id)) |> as(module) + end + ` + } + variationModuleIdData(name: string) { + return dedent` + # with module / id / data + def ${name}(module, id, data) when is_atom(module) do + ${name}(to_thing(module, id), data) |> as(module) + end + ` + } + variationModuleDataCreation(name: string) { + return dedent` + # with module / data (create / insert) - single + def ${name}(module, data) when is_atom(module) and is_map(data) do + # For some reason we get an array, extract the first element + ${name}(to_thing(module), data) |> Res.first() |> as(module) + end + + # with module / data (create / insert) - list + def ${name}(module, data) when is_atom(module) and is_list(data) do + ${name}(to_thing(module), data) |> as(module) + end + ` + } + variationModuleData(name: string) { + return dedent` + # with module / data (updates / changes) (map) + def ${name}(module, data) when is_atom(module) and is_map(data) do + ${name}(to_thing(module), data) |> as(module) + end + # with module / data (updates / changes) (list) + def ${name}(module, data) when is_atom(module) and is_list(data) do + ${name}(to_thing(module), data) |> as(module) + end + ` + } + + genQueryMethod() { + let content = this.queryMethod() + content = lpad(content, " ") + this.push(content) + } + + queryMethod() { + return dedent` + + ${this.methodBanner("query")} + + def query(sql) when is_binary(sql) do + query(sql, %{}) + end + + def query(sql, vars) when is_binary(sql) and is_map(vars) do + Logger.info("SQL: #{inspect(sql)}, #{inspect(vars)}") + Surrealix.query(current_repo(), sql, vars) + end + + def query(module, sql) when is_atom(module) and is_binary(sql) do + query(module, sql, %{}) + end + + def query(module, sql, vars) when is_atom(module) and is_binary(sql) and is_map(vars) do + query(sql, vars) |> Res.first() |> as(module) + end + ` + } + + genFrontStatic() { + let content = dedent` + + alias Surreal.Config + alias Surreal.Rec + alias Surreal.Res + + require Logger + + @default_repo [0, 0] + + def start_link([]) do + init() + end + + def init() do + with {:ok, pid} <- Surreal.Conn.get_pid(@default_repo, init_opts()) do + Surrealix.wait_until_auth_ready(pid) + {:ok, pid} + end + end + + defp init_opts do + [ + hostname: Config.host(), + port: Config.port(), + on_auth: fn pid, _state -> + IO.puts("PID: #{inspect(pid)}") + Surrealix.signin(pid, %{user: Config.user(), pass: Config.pass()}) + Surrealix.use(pid, Config.ns(), Config.db()) + end + ] + end + + ### API ### + + def as(res, struct_module) do + Res.as(res, struct_module) + end + + def live_query(sql, vars \\\\ %{}, callback) do + Surrealix.live_query(current_repo(), sql, vars, callback) + end + + ### + ### QUERY STRUCT + ### + alias Surreal.Query + + def all(%Query{} = q) do + {sql, vars} = Query.to_raw_sql(q) + + # unwraps nested response from SurrealDB without raising on errors + query(sql, vars) + |> Surreal.Result.from_raw_query() + |> Maxo.Result.map(&Enum.at(&1, 0)) + |> Maxo.Result.flatten() + end + + def all!(%Query{} = q) do + all(q) |> Maxo.Result.unwrap!() + end + + def one!(%Query{} = q) do + all!(q) |> Enum.at(0) + end + ` + + content = lpad(content, " ") + this.push(content) + } + + genAdminHelpers() { + let content = dedent` + + ### + ### Admin API ######### + ### + + def current_repo() do + dynamic_repo() || default_repo() + end + + def dynamic_repo do + Maxo.ProcDict.get_with_ancestors(:surreal_repo) + end + + def default_repo do + Surreal.Conn.get_pid(@default_repo) |> Res.ok() + end + + def put_dynamic_repo(pid) do + Maxo.ProcDict.put(:surreal_repo, pid) + end + + defp to_thing(module) do + extract_table_name(module) + end + + defp to_thing(module, id) do + Rec.recid(extract_table_name(module), id) + end + + defp extract_table_name(module) do + # workaround to make sure the module is properly loaded (sometimes it is not loaded yet) + Code.ensure_compiled(module) + + cond do + has_function(module, :__table__, 0) -> module.__table__() + has_function(module, :__schema__, 1) -> module.__schema__(:source) + true -> raise "NOT POSSIBLE TO MAP #{module}" + end + end + + defp module_data_from_changeset(changesets) when is_list(changesets) and length(changesets) > 0 do + module = Surreal.Dumper.module_from_changeset!(:insert, Enum.at(changesets, 0)) + data_list = Enum.map(changesets, &get_data_chset/1) + {module, data_list} + end + + defp module_data_from_changeset(changeset) do + module = Surreal.Dumper.module_from_changeset!(:insert, changeset) + data = get_data_chset(changeset) + {module, data} + end + + def struct_to_changeset(struct) do + schema = struct.__struct__ + fullchanges = Map.take(struct, schema.__schema__(:fields)) + Ecto.Changeset.change(struct(schema), fullchanges) + end + + def get_data_chset(chset) do + {:ok, data} = Surreal.Dumper.from_changeset(chset) + Enum.into(data, %{}) + end + + defp has_function(mod, fun, arity) do + Kernel.function_exported?(mod, fun, arity) + end + + ` + + content = lpad(content, " ") + this.push(content) + } +} + +const generator = new RepoGenerator(); +generator.run(); +// console.log(generator.content) +const me = path.join(import.meta.dir, "../..", "lib/surreal/repo.ex") +Bun.write(me, generator.content) \ No newline at end of file diff --git a/gen/src/api.ts b/gen/src/api.ts new file mode 100644 index 0000000..e69de29 diff --git a/gen/src/db/types.ts b/gen/src/db/types.ts new file mode 100644 index 0000000..40051db --- /dev/null +++ b/gen/src/db/types.ts @@ -0,0 +1,66 @@ +import { z } from "zod"; +const columnSchema = z.discriminatedUnion("kind", [ + z.object({ kind: z.literal("varchar"), size: z.number() }), + z.object({ kind: z.literal("uuid") }), + z.object({ kind: z.literal("ulid") }), + z.object({ kind: z.literal("decimal"), precision: z.number(), decimal: z.number() }), + z.object({ kind: z.literal("bigint") }), + z.object({ kind: z.literal("boolean") }), + z.object({ kind: z.literal("datetime") }), + z.object({ kind: z.literal("enum"), values: z.array(z.string()) }), +]); + +const tableSchema = z.object({ + name: z.string(), + fields: z.array(z.object({ + name: z.string(), + pk: z.boolean().default(false).optional(), + kind: columnSchema + })) +}) +type TableSchema = z.infer + +const relationSchema = z.object({ + src_table: z.string(), + src_field: z.string(), + dest_table: z.string(), + dest_field: z.string(), + kind: z.union([z.literal("one2one"), z.literal("one2many"), z.literal("many2one")]) +}) + +type RelationSchema = z.infer + +const userTable: TableSchema = { + name: "user", + fields: [ + { name: "id", pk: true, kind: { kind: "ulid" } }, + { name: "email", kind: { kind: "varchar", size: 300 } } + ] +} +const sprintTable: TableSchema = { + name: "sprint", + fields: [ + { name: "id", pk: true, kind: { kind: "ulid" } }, + { name: "name", kind: { kind: "varchar", size: 50 } } + ] +} +const sprintUploadTable: TableSchema = { + name: "sprint_upload", + fields: [ + { name: "id", pk: true, kind: { kind: "ulid" } }, + { name: "sprint_name", kind: { kind: "varchar", size: 50 } }, + { name: "sprint_id", kind: { kind: "ulid" } }, + ] +} + +const relations: RelationSchema[] = [ + { src_table: "sprint_upload", src_field: "sprint_id", dest_table: "sprint", dest_field: "id", kind: "many2one" } +] + + + +const DbSchema = { + tables: [userTable, sprintTable, sprintUploadTable], + relations: relations, +} +console.log(Bun.inspect(DbSchema)) \ No newline at end of file diff --git a/gen/src/db/types2.ts b/gen/src/db/types2.ts new file mode 100644 index 0000000..c9f97a4 --- /dev/null +++ b/gen/src/db/types2.ts @@ -0,0 +1,135 @@ +import { z } from "zod"; + +const columnSchema = z.union([ + z.literal("uuid"), + z.literal("ulid"), + z.literal("boolean"), + z.literal("int"), + z.literal("bigint"), + z.literal("datetime"), + z.tuple([ + z.literal("varchar"), + z.number() // size + ]), + z.tuple([ + z.literal("decimal"), + z.number(), // precision, + z.number() // decimal + ]), + z.tuple([ + z.literal("enum"), + z.array(z.string()), // values + ]), +]) + +const fieldSchema = z.object({ + name: z.string(), + note: z.string().default("").optional(), + pk: z.boolean().default(false).optional(), + optional: z.boolean().default(false).optional(), + kind: columnSchema +}) +type FieldSchema = z.infer + +const tableSchema = z.object({ + name: z.string(), + note: z.string().optional(), + fields: z.array(fieldSchema) +}) +type TableSchema = z.infer + +const relationshipKind = z.union([z.literal("one2one"), z.literal("one2many"), z.literal("many2one")]) + +type RelationshipKind = z.infer +const relationSchema = z.object({ + note: z.string().optional(), + src_table: z.string(), + src_field: z.string(), + dest_table: z.string(), + dest_field: z.string(), + kind: relationshipKind, +}) + +type RelationSchema = z.infer + +const rel = (src: string, dest: string, kind: RelationshipKind, note = "") => { + const [scr_table, src_field] = src.split(".") + const [dest_table, dest_field] = dest.split(".") + return { + src_table: scr_table, + src_field: src_field, + dest_table: dest_table, + dest_field: dest_field, + kind: kind, + note: note + } +} + +////////////////////////////////////// TABLES /////////////////////////////////////// + +const timestampFields: FieldSchema[] = [ + { name: "created_at", kind: "datetime" }, + { name: "updated_at", kind: "datetime" }, +] + +const userTable: TableSchema = { + name: "user", + fields: [ + { name: "id", pk: true, kind: "ulid" }, + { name: "email", kind: ["varchar", 300] }, + ...timestampFields + ] +} +const sprintTable: TableSchema = { + name: "sprint", + fields: [ + { name: "id", pk: true, kind: "ulid" }, + { name: "name", kind: ["varchar", 50] }, + ...timestampFields + ] +} +const sprintUploadTable: TableSchema = { + name: "sprint_upload", + fields: [ + { name: "id", pk: true, kind: "ulid" }, + { name: "sprint_name", kind: ["varchar", 50] }, + { name: "sprint_id", kind: "ulid" }, + ...timestampFields + ] +} + +const ecuUnitTable: TableSchema = { + name: "ecu_unit", + fields: [ + { name: "id", pk: true, kind: ["varchar", 50] }, + { name: "type", pk: true, kind: ["enum", ["telematic", "powertrain", "other"]] }, + ...timestampFields + ] +} + +const ecuSwBlockTable: TableSchema = { + name: "ecu_sw_block", + fields: [ + { name: "id", pk: true, kind: "ulid" }, + { name: "ecu_unit_id", kind: ["varchar", 50] }, + { name: "type", pk: true, kind: ["enum", ["telematic", "powertrain", "other"]] }, + ...timestampFields + ] +} + +const relations: RelationSchema[] = [ + rel("sprint_upload.sprint_id", "sprint.id", "many2one", "sprint can have multiple uploads"), + rel("ecu_sw_block.ecu_unit_id", "ecu_unit.id", "many2one", "ecu unit have multiple software blocks") +] + +const DbSchema = { + tables: [ + userTable, + sprintTable, + ecuUnitTable, + ecuSwBlockTable, + sprintUploadTable, + ], + relations: relations, +} +console.log(Bun.inspect(DbSchema)) \ No newline at end of file diff --git a/gen/src/genCI.ts b/gen/src/genCI.ts new file mode 100644 index 0000000..1c91a96 --- /dev/null +++ b/gen/src/genCI.ts @@ -0,0 +1,176 @@ +const yaml = require("js-yaml"); +const fs = require("fs"); + +const setupElixirSteps = [ + { + name: "Cache deps", + id: "cache-deps", + uses: "actions/cache@v3", + env: { + "cache-name": "cache-elixir-deps", + }, + with: { + path: "deps", + key: "${{ runner.os }}-mix-${{ env.MIX_ENV }}-${{ env.cache-name }}-${{ hashFiles('mix.lock') }}", + "restore-keys": "${{ runner.os }}-mix-${{ env.MIX_ENV }}-${{ env.cache-name }}-\n", + }, + }, + { + name: "Cache compiled build", + id: "cache-build", + uses: "actions/cache@v3", + env: { + "cache-name": "cache-compiled-build", + }, + with: { + path: "_build", + key: "${{ runner.os }}-mix-${{ env.MIX_ENV }}-${{ env.cache-name }}-${{ hashFiles('mix.lock') }}", + "restore-keys": + "${{ runner.os }}-mix-${{ env.MIX_ENV }}-${{ env.cache-name }}-\n${{ runner.os }}-mix-${{ env.MIX_ENV }}-\n${{ runner.os }}-mix\n", + }, + }, + { + name: "Set up Elixir", + uses: "erlef/setup-beam@v1", + with: { + "elixir-version": "1.16.0", + "otp-version": "26.1.2", + }, + }, + { + name: "Install dependencies", + run: "mix deps.get", + }, + { + name: "Setup .env file", + run: "bin/ci-setup.sh", + }, +]; +const checkoutSteps = [ + { + uses: "actions/checkout@v3", + }, +]; + +const startSurrealSteps = [ + { + name: "Start SurrealDB docker image", + run: "bin/ci-docker-surreal-restart.sh", + }, +]; + +const stopSurrealSteps = [ + { + name: "Stop SurrealDB docker image", + run: "bin/ci-docker-surreal-stop.sh", + }, +]; + + +const checkElixirFmt = [ + { + name: "Run format check", + run: "mix format --check-formatted", + }, +] + +const compileElixirCode = [ + { + name: "Compile Elixir code", + run: "mix compile", + }, +] + +const runElixirTest = [ + { + name: "Run Elixir tests", + run: "mix test", + }, +] + +const testEnv = { + MIX_ENV: "test", + ELIXIR_ENV: "test", + DATABASE_HOST: "localhost", + DATABASE_USER: "postgres", + DATABASE_PORT: 5432, + DATABASE_PASSWORD: "postgres", +}; + +const postgresService = { + image: "postgres:15.4", + env: { + POSTGRES_PASSWORD: "postgres", + }, + ports: ["5432:5432"], + options: "--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5", +}; + +const job = { + name: "CI", + on: { + push: { + // branches: ['main'], + }, + pull_request: {}, + }, + jobs: { + "mix-format": { + name: "Mix Format", + "runs-on": "ubuntu-latest", + env: testEnv, + steps: [ + ...checkoutSteps, + ...setupElixirSteps, + // first we check fmt, if that fails, we do not need to run expensive compile step + ...checkElixirFmt, + // we compile here, that way cache is reusable for tests, even if they fail! + ...compileElixirCode, + ], + }, + "mix-test": { + name: "Mix Test", + "runs-on": "ubuntu-latest", + // wait until mix-format finishes, so we can reuse the caches! + needs: "mix-format", + env: testEnv, + services: { + postgres: postgresService, + }, + steps: [ + ...checkoutSteps, + ...setupElixirSteps, + ...startSurrealSteps, + ...runElixirTest, + ...stopSurrealSteps, + ], + }, + }, +}; + +// console.log(import.meta.dir); +const ciYamlPath = `${import.meta.dir}/../../.github/workflows/ci.yml`; + +class GeneratorCI { + run() { + const output = yaml.dump(job, { noRefs: true }); + const banner = "### GENERATED by gen/genCI.ts!\n\n"; + const fullContent = banner + output; + console.log(fullContent); + fs.writeFileSync(ciYamlPath, fullContent, "utf8"); + } + + dumpCurrent() { + try { + const doc = yaml.load(fs.readFileSync(ciYamlPath, "utf8")); + const res = JSON.stringify(doc, null, 2); + console.log(res); + } catch (e) { + console.log(e); + } + } +} + +const generator = new GeneratorCI(); +generator.run(); +// generator.dumpCurrent(); diff --git a/gen/src/utils.ts b/gen/src/utils.ts new file mode 100644 index 0000000..f629d15 --- /dev/null +++ b/gen/src/utils.ts @@ -0,0 +1,36 @@ +/** + * Prepend each line for the string with some prefix + * @param v + * @param toPad + * @returns + */ +export const lpad = (v: string, toPad: string) => { + return v + .split("\n") + .map((line) => { + return toPad + line; + }) + .join("\n"); +}; + +/** + * Upcase first char in a string + * @param v + * @returns + */ +export const constantize = (v: string) => { + const upper = v.substr(0, 1); + return upper.toUpperCase() + v.slice(1, v.length); +}; + +export const escapeFn = (text: string) => { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); +}; + +/** + * Full console log output for serializable JS objects + * @param a + */ +export const debug = (a: unknown) => { + console.log(JSON.stringify(a, null, 4)); +}; \ No newline at end of file diff --git a/gen/tsconfig.json b/gen/tsconfig.json new file mode 100644 index 0000000..7556e1d --- /dev/null +++ b/gen/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "noEmit": true, + "composite": true, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "types": [ + "bun-types" // add Bun global + ] + } +}