cloesce 0.0.3-fix.6 → 0.0.4-unstable.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/README.md CHANGED
@@ -1,25 +1,23 @@
1
- # cloesce (experimental, `v0.0.3`)
1
+ # cloesce (unstable, `v0.0.4`)
2
2
 
3
- Cloesce is a full stack compiler for the Cloudflare developer platform, allowing class definitions in high level languages to serve as the metadata basis for databases, a backend REST API, a frontend API client, and Cloudflare infrastructure.
3
+ Cloesce is a full stack compiler for the Cloudflare developer platform, allowing class definitions in high level languages to serve as a metadata basis to create a database schema, backend REST API, frontend API client, and Cloudflare infrastructure (as of v0.0.4, D1 + Workers).
4
4
 
5
5
  Cloesce is working towards an alpha MVP (v0.1.0), with the general milestones being [here](https://cloesce.pages.dev/schreiber/v0.1.0_milestones/).
6
6
 
7
7
  Internal documentation going over design decisions and general thoughts for each milestone can be found [here](https://cloesce.pages.dev/).
8
8
 
9
- # Documentation `v0.0.3`
10
-
11
- Note that this version is very unstable (ie, it passes our set of happy-path tests).
9
+ # Documentation `v0.0.4`
12
10
 
13
11
  ## Getting Started
14
12
 
15
- `v0.0.3` supports only Typescript-to-Typescript projects. An example project is shown [here](https://github.com/bens-schreiber/cloesce/tree/main/examples).
13
+ `v0.0.4` supports only Typescript-to-Typescript projects. An example project is shown [here](https://github.com/bens-schreiber/cloesce/tree/main/examples).
16
14
 
17
15
  1. NPM
18
16
 
19
17
  - Create an NPM project and install cloesce
20
18
 
21
19
  ```sh
22
- npm i cloesce@0.0.3-fix.3
20
+ npm i cloesce@0.0.4-unstable.0
23
21
  ```
24
22
 
25
23
  2. TypeScript
@@ -42,19 +40,19 @@ npm i cloesce@0.0.3-fix.3
42
40
 
43
41
  3. Cloesce Config
44
42
 
45
- - Create a `cloesce.config.json` with the following values:
43
+ - Create a `cloesce.config.json` with the following keys:
46
44
 
47
45
  ```json
48
46
  {
49
- "source": "./src", // or whatever src you want
50
- "workersUrl": "http://localhost:5002/api", // or whatever url you want
51
- "clientUrl": "http://localhost:5002/api"
47
+ "source": "./src",
48
+ "workersUrl": "http://localhost:5000/api",
49
+ "clientUrl": "http://localhost:5173/api"
52
50
  }
53
51
  ```
54
52
 
55
53
  4. Vite
56
54
 
57
- - `v0.0.3` does not yet support middleware, so you'll run into CORs problems. A vite proxy in some `vite.config.ts` can fix this:
55
+ To prevent CORS issues, a Vite proxy can be used for the frontend:
58
56
 
59
57
  ```ts
60
58
  import { defineConfig } from "vite";
@@ -74,7 +72,7 @@ export default defineConfig({
74
72
 
75
73
  5. Wrangler Config
76
74
 
77
- - `v0.0.3` will generate the required areas of your wrangler config. A full config looks like this:
75
+ - `v0.0.4` will generate the required areas of your wrangler config. A full config looks like this:
78
76
 
79
77
  ```toml
80
78
  compatibility_date = "2025-10-02"
@@ -89,12 +87,19 @@ database_name = "example"
89
87
 
90
88
  ## A Simple Model
91
89
 
92
- A model is a type which represents a database table, database view, backend api, frontend client and Cloudflare infrastructure (D1 + Workers).
90
+ A model is a type which represents:
91
+
92
+ - a database table,
93
+ - database views
94
+ - REST API
95
+ - Client API
96
+ - Cloudflare infrastructure (D1 + Workers)
93
97
 
94
- Suprisingly, it's pretty compact. A basic scalar model (being, without foreign keys) looks like this:
98
+ Suprisingly, it's pretty compact. A basic model looks like this:
95
99
 
96
100
  ```ts
97
101
  // horse.cloesce.ts
102
+ import { D1, GET, POST, PrimaryKey } from "cloesce/backend";
98
103
 
99
104
  @D1
100
105
  export class Horse {
@@ -104,7 +109,7 @@ export class Horse {
104
109
  name: string | null;
105
110
 
106
111
  @POST
107
- async neigh(): Promise<string> {
112
+ neigh(): string {
108
113
  return `i am ${this.name}, this is my horse noise`;
109
114
  }
110
115
  }
@@ -113,25 +118,34 @@ export class Horse {
113
118
  - `@D1` denotes that this is a SQL Table
114
119
  - `@PrimaryKey` sets the SQL primary key. All models require a primary key.
115
120
  - `@POST` reveals the method as an API endpoint with the `POST` HTTP Verb.
121
+ - All Cloesce models need to be under a `.cloesce.ts` file.
116
122
 
117
- After running `cloesce run`, you will get a fully generated project that can be ran with:
123
+ To compile this model into a working full stack application, Cloesce must undergo both **compilation** and **migrations**. Compilation is the process of extracting the metadata language that powers Cloesce, ensuring it is a valid program, and then producing code to orchestrate the program across different domains (database, backend, frontend, cloud). Migrations utilize the history of validated metadata to create SQL code, translating the evolution of your models.
118
124
 
119
- ```sh
120
- # migrate wrangler
121
- npx wrangler d1 migrations apply proj-name
125
+ To compile, run `npx cloesce compile`.
126
+
127
+ To create a migration, run `npx cloesce migrate <name>`.
122
128
 
123
- # build
124
- npx wrangler build
129
+ After running the above commands, you will have a full project capable of being ran with:
125
130
 
126
- # run wrangler
127
- npx wrangler dev --port 5000
131
+ ```sh
132
+ # Apply the generated migrations
133
+ npx wrangler d1 migrations apply <db-name>
134
+
135
+ # Run the backend
136
+ npx wrangler dev
128
137
  ```
129
138
 
130
- Note the output in the `.generated/` dir:
139
+ Note the output in the `.generated/` dir. These values should not be committed to git, as they depend on the file system of the machine running it.
131
140
 
132
141
  - `client.ts` is an importable API with all of your backend types and endpoints
133
- - `migrations/*.sql` is the generated SQL code (note, migrations beyond the initial aren't really supported yet)
134
142
  - `workers.ts` is the workers entrypoint.
143
+ - `cidl.json` is the working metadata for the project
144
+
145
+ Note the output in `migrations`, ex after running `npx cloesce migrate Initial`
146
+
147
+ - `<date>_Initial.json` contains all model information necessary from SQL
148
+ - `<date>_Initial.sql` contains the acual SQL migrations. In this early version of Cloesce, it's important to check migrations every time.
135
149
 
136
150
  ## Features
137
151
 
@@ -142,6 +156,8 @@ In order to interact with your database, you will need to define a WranglerEnv
142
156
  ```ts
143
157
  // horse.cloesce.ts
144
158
 
159
+ import { WranglerEnv } from "cloesce/backend";
160
+
145
161
  @WranglerEnv
146
162
  export class Env {
147
163
  db: D1Database; // only one DB is supported for now-- make sure it matches the name in `wrangler.toml`
@@ -161,118 +177,117 @@ export class Horse {
161
177
 
162
178
  @POST
163
179
  async neigh(@Inject env: WranglerEnv): Promise<string> {
164
- env.db.prepare(...);
180
+ await env.db.prepare(...);
165
181
 
166
182
  return `i am ${this.name}, this is my horse noise`;
167
183
  }
168
184
  }
169
185
  ```
170
186
 
171
- ## Foreign Keys, One to One, Data Sources
187
+ ### Foreign Keys, One to One, Data Sources
172
188
 
173
- More complex model relationships are permitted via the `@ForeignKey`, `@OneToOne / @OneToMany @ManyToMany` and `@DataSource` decorators.
189
+ Complex model relationships are permitted via the `@ForeignKey`, `@OneToOne / @OneToMany @ManyToMany` and `@DataSource` decorators.
174
190
  Foreign keys are scalar attributes which must reference some other model's primary key:
175
191
 
176
192
  ```ts
177
193
  @D1
178
- export class B {
194
+ export class Dog {
179
195
  @PrimaryKey
180
196
  id: number;
181
197
  }
182
198
 
183
199
  @D1
184
- export class A {
200
+ export class Person {
185
201
  @PrimaryKey
186
202
  id: number;
187
203
 
188
- @ForeignKey(B)
189
- bId: number;
204
+ @ForeignKey(Dog)
205
+ dogId: number;
190
206
  }
191
207
  ```
192
208
 
193
- This representation is true to the underlying SQL table: `A` has a column `bId` which is a foreign key to `B`. Cloesce allows you to actually join these tables together in your model representation:
209
+ This representation is true to the underlying SQL table: `Person` has a column `dogId` which is a foreign key to `Dog`. Cloesce allows you to actually join these tables together in your model representation:
194
210
 
195
211
  ```ts
196
212
  @D1
197
- export class B {
213
+ export class Dog {
198
214
  @PrimaryKey
199
215
  id: number;
200
216
  }
201
217
 
202
218
  @D1
203
- export class A {
219
+ export class Person {
204
220
  @PrimaryKey
205
221
  id: number;
206
222
 
207
- @ForeignKey(B)
208
- bId: number;
223
+ @ForeignKey(Dog)
224
+ dogId: number;
209
225
 
210
- @OneToOne("bId") // references A.bId
211
- b: B | undefined; // This value is a "navigation property", which may or may not exist at runtime
226
+ @OneToOne("dogId") // references Person.dogId
227
+ dog: Dog | undefined; // This value is a "navigation property", which may or may not exist at runtime
212
228
  }
213
229
  ```
214
230
 
215
- In `v0.0.3`, there are no defaults, only very explicit decisons. Because of that, navigation properties won't exist at runtime unless you tell them to. Cloesce does this via a `DataSource`, which describes the foreign key dependencies you wish to include. All scalar properties are included by default and cannot be excluded.
231
+ In `v0.0.4`, there are no defaults, only very explicit decisons. Because of that, navigation properties won't exist at runtime unless you tell them to. Cloesce does this via a `DataSource`, which describes the foreign key dependencies you wish to include. All scalar properties are included by default and cannot be excluded.
216
232
 
217
233
  ```ts
218
234
  @D1
219
- export class B {
235
+ export class Dog {
220
236
  @PrimaryKey
221
237
  id: number;
222
238
  }
223
239
 
224
240
  @D1
225
- export class A {
241
+ export class Person {
226
242
  @PrimaryKey
227
243
  id: number;
228
244
 
229
- @ForeignKey(B)
230
- bId: number;
245
+ @ForeignKey(Dog)
246
+ dogId: number;
231
247
 
232
- @OneToOne("bId")
233
- b: B | undefined;
248
+ @OneToOne("dogId")
249
+ dog: Dog | undefined;
234
250
 
235
251
  @DataSource
236
- static readonly default: IncludeTree<A> = {
237
- b: {}, // says: on model population, join A's B
252
+ static readonly default: IncludeTree<Person> = {
253
+ dog: {}, // says: on model population, join Persons's Dog
238
254
  };
239
255
  }
240
256
  ```
241
257
 
242
- Datasources are just SQL views and can be invoked in your queries. They are aliased in such a way that its identical to object properties. The frontend chooses which datasource to use in it's API client. `null` is a valid option, meaning no joins will occur.
258
+ Data sources are just SQL views and can be invoked in your queries. They are aliased in such a way that its similiar to object properties. The frontend chooses which datasource to use in it's API client. `null` is a valid option, meaning no joins will occur.
243
259
 
244
260
  ```ts
245
261
  @D1
246
- export class A {
262
+ export class Person {
247
263
  @PrimaryKey
248
264
  id: number;
249
265
 
250
- @ForeignKey(B)
251
- bId: number;
266
+ @ForeignKey(Dog)
267
+ dogId: number;
252
268
 
253
- @OneToOne("bId")
254
- b: B | undefined;
269
+ @OneToOne("dogId")
270
+ dog: Dog | undefined;
255
271
 
256
272
  @DataSource
257
- static readonly default: IncludeTree<A> = {
258
- b: {},
273
+ static readonly default: IncludeTree<Person> = {
274
+ dog: {},
259
275
  };
260
276
 
261
277
  @GET
262
- static async get(id: number, @Inject env: WranglerEnv): Promise<A> {
278
+ static async get(id: number, @Inject env: WranglerEnv): Promise<Person> {
263
279
  let records = await env.db
264
- .prepare("SELECT * FROM [A.default] WHERE [A.id] = ?") // A.default is the SQL view generated from the IncludeTree
280
+ .prepare("SELECT * FROM [Person.default] WHERE [Person.id] = ?") // Person.default is the SQL view generated from the IncludeTree
265
281
  .bind(id)
266
282
  .run();
267
283
 
268
- // modelsFromSql is a provided function to turn sql rows into an object.
269
- // More ORM functions will be expanded on in v0.0.4.
270
- return modelsFromSql(A, records.results, A.default)[0] as A;
284
+ let persons = Orm.fromSql(Person, records.results, Person.default);
285
+ return persons.value[0];
271
286
  }
272
287
  }
273
288
  ```
274
289
 
275
- Note: In later versions, nearly all of the code from the example above won't be needed (we can usually infer primary keys, foreign keys, relationships, have default made include trees, and generate CRUD methods like `get`).
290
+ Note that the `get` code can be simplified using CRUD methods or ORM primitives.
276
291
 
277
292
  ### One to Many
278
293
 
@@ -316,8 +331,6 @@ export class Dog {
316
331
 
317
332
  ### Many to Many
318
333
 
319
- NOTE: `M:M` relationships have a [bug](https://github.com/bens-schreiber/cloesce/issues/88) in `v0.0.3`, but the syntax is as follows:
320
-
321
334
  ```ts
322
335
  @D1
323
336
  export class Student {
@@ -333,11 +346,74 @@ export class Course {
333
346
  @PrimaryKey
334
347
  id: number;
335
348
 
336
- @ManyToMany("StudentsCourses")
349
+ @ManyToMany("StudentsCourses") // same unique id => same jct table.
337
350
  students: Student[];
338
351
  }
339
352
  ```
340
353
 
354
+ ### ORM Methods
355
+
356
+ Cloesce provides a suite of ORM methods for getting, listing, updating and inserting models.
357
+
358
+ #### Upsert
359
+
360
+ ```ts
361
+ @D1
362
+ class Horse {
363
+ // ...
364
+
365
+ @POST
366
+ static async post(@Inject { db }: Env, horse: Horse): Promise<Horse> {
367
+ const orm = Orm.fromD1(db);
368
+ await orm.upsert(Horse, horse, null);
369
+ return (await orm.get(Horse, horse.id, null)).value;
370
+ }
371
+ }
372
+ ```
373
+
374
+ #### List, Get
375
+
376
+ ```ts
377
+ @D1
378
+ class Horse {
379
+ // ...
380
+ @GET
381
+ static async get(@Inject { db }: Env, id: number): Promise<Horse> {
382
+ const orm = Orm.fromD1(db);
383
+ return (await orm.get(Horse, id, "default")).value;
384
+ }
385
+
386
+ @GET
387
+ static async list(@Inject { db }: Env): Promise<Horse[]> {
388
+ const orm = Orm.fromD1(db);
389
+ return (await orm.list(Horse, "default")).value;
390
+ }
391
+ }
392
+ ```
393
+
394
+ ### CRUD Methods
395
+
396
+ Generic GET, POST, PATCH (and in a future version, DEL) boilerplate methods do not need to be copied around. Cloesce supports CRUD generation, a syntactic sugar that adds the methods to the compiler output.
397
+
398
+ ```ts
399
+ @CRUD(["POST", "GET", "LIST"])
400
+ @D1
401
+ export class CrudHaver {
402
+ @PrimaryKey
403
+ id: number;
404
+ name: string;
405
+ }
406
+ ```
407
+
408
+ which will generate client API methods like:
409
+
410
+ ```ts
411
+ static async get(
412
+ id: number,
413
+ dataSource: "none" = "none",
414
+ ): Promise<HttpResult<CrudHaver>>
415
+ ```
416
+
341
417
  ### Plain Old Objects
342
418
 
343
419
  Simple non-model objects can be returned and serialized from a model method:
@@ -355,7 +431,7 @@ export class Cat {
355
431
  id: number;
356
432
 
357
433
  @GET
358
- async query(): Promise<CatStuff> {
434
+ query(): CatStuff {
359
435
  return {
360
436
  catFacts: ["cats r cool"],
361
437
  catNames: ["reginald"]
@@ -384,16 +460,24 @@ class Foo {
384
460
 
385
461
  ## Unit Tests
386
462
 
387
- - `src/extractor/ts` run `npm test`
463
+ - `src/frontend/ts` run `npm test`
388
464
  - `src/generator` run `cargo test`
389
465
 
390
466
  ## Integration Tests
391
467
 
392
- - To run the regression tests: `cargo run --bin test regression`
393
- - To run the pass fail extractor tests: `cargo run --bin test run-fail`
468
+ - Regression tests: `cargo run --bin test regression`
469
+ - Pass fail extractor tests: `cargo run --bin test run-fail`
394
470
 
395
471
  Optionally, pass `--check` if new snapshots should not be created.
396
472
 
473
+ To update integration snapshots, run:
474
+
475
+ - `cargo run --bin update`
476
+
477
+ To delete any generated snapshots run:
478
+
479
+ - `cargo run --bin update -- -d`
480
+
397
481
  ## E2E
398
482
 
399
483
  - `tests/e2e` run `npm test`
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import { WASI } from "node:wasi";
3
3
  import fs from "node:fs";
4
4
  import { readFile } from "fs/promises";
5
5
  import path from "node:path";
6
- import { command, run, subcommands, flag, option, optional, string, } from "cmd-ts";
6
+ import { command, run, subcommands, flag, string, positional, option, optional, } from "cmd-ts";
7
7
  import { Project } from "ts-morph";
8
8
  import { CidlExtractor } from "./extractor/extract.js";
9
9
  import { ExtractorError, ExtractorErrorCode, getErrorInfo } from "./common.js";
@@ -35,9 +35,9 @@ const cmds = subcommands({
35
35
  args: [
36
36
  "generate",
37
37
  "all",
38
+ path.join(outputDir, "cidl.pre.json"),
38
39
  path.join(outputDir, "cidl.json"),
39
40
  "wrangler.toml",
40
- "migrations/migrations.sql",
41
41
  path.join(outputDir, "workers.ts"),
42
42
  path.join(outputDir, "client.ts"),
43
43
  config.clientUrl,
@@ -48,89 +48,9 @@ const cmds = subcommands({
48
48
  await generate(allConfig);
49
49
  },
50
50
  }),
51
- wrangler: command({
52
- name: "wrangler",
53
- description: "Generate wrangler.toml configuration",
54
- args: {},
55
- handler: async () => {
56
- await generate({
57
- name: "wrangler",
58
- wasmFile: "generator.wasm",
59
- args: ["generate", "wrangler", "wrangler.toml"],
60
- });
61
- },
62
- }),
63
- d1: command({
64
- name: "d1",
65
- description: "Generate database schema",
66
- args: {},
67
- handler: async () => {
68
- const config = loadCloesceConfig(process.cwd());
69
- const outputDir = config.outputDir ?? ".generated";
70
- await generate({
71
- name: "d1",
72
- wasmFile: "generator.wasm",
73
- args: [
74
- "generate",
75
- "d1",
76
- path.join(outputDir, "cidl.json"),
77
- "migrations/migrations.sql",
78
- ],
79
- });
80
- },
81
- }),
82
- workers: command({
83
- name: "workers",
84
- description: "Generate workers TypeScript",
85
- args: {},
86
- handler: async () => {
87
- const config = loadCloesceConfig(process.cwd());
88
- const outputDir = config.outputDir ?? ".generated";
89
- if (!config.workersUrl) {
90
- console.error("Error: workersUrl must be defined in cloesce.config.json");
91
- process.exit(1);
92
- }
93
- await generate({
94
- name: "workers",
95
- wasmFile: "generator.wasm",
96
- args: [
97
- "generate",
98
- "workers",
99
- path.join(outputDir, "cidl.json"),
100
- path.join(outputDir, "workers.ts"),
101
- "wrangler.toml",
102
- config.workersUrl,
103
- ],
104
- });
105
- },
106
- }),
107
- client: command({
108
- name: "client",
109
- description: "Generate client TypeScript",
110
- args: {},
111
- handler: async () => {
112
- const config = loadCloesceConfig(process.cwd());
113
- const outputDir = config.outputDir ?? ".generated";
114
- if (!config.clientUrl) {
115
- console.error("Error: clientUrl must be defined in cloesce-config.json");
116
- process.exit(1);
117
- }
118
- await generate({
119
- name: "client",
120
- wasmFile: "generator.wasm",
121
- args: [
122
- "generate",
123
- "client",
124
- path.join(outputDir, "cidl.json"),
125
- path.join(outputDir, "client.ts"),
126
- config.clientUrl,
127
- ],
128
- });
129
- },
130
- }),
131
51
  extract: command({
132
52
  name: "extract",
133
- description: "Extract models and write cidl.json only",
53
+ description: "Extract models and write cidl.pre.json",
134
54
  args: {
135
55
  projectName: option({
136
56
  long: "project-name",
@@ -141,7 +61,6 @@ const cmds = subcommands({
141
61
  long: "out",
142
62
  short: "o",
143
63
  type: optional(string),
144
- description: "Output path (default: .generated/cidl.json)",
145
64
  }),
146
65
  inp: option({
147
66
  long: "in",
@@ -169,6 +88,54 @@ const cmds = subcommands({
169
88
  await extract({ ...args });
170
89
  },
171
90
  }),
91
+ migrate: command({
92
+ name: "migrate",
93
+ description: "Creates a database migration.",
94
+ args: {
95
+ name: positional({ type: string, displayName: "name" }),
96
+ debug: flag({
97
+ long: "debug",
98
+ short: "d",
99
+ description: "Show debug output",
100
+ }),
101
+ },
102
+ handler: async (args) => {
103
+ const config = loadCloesceConfig(process.cwd(), args.debug);
104
+ const cidlPath = path.join(config.outputDir ?? ".generated", "cidl.json");
105
+ if (!fs.existsSync(cidlPath)) {
106
+ console.error("Err: No cloesce file found, have you ran `cloesce compile`?");
107
+ process.exit(1);
108
+ }
109
+ const migrationsPath = "./migrations";
110
+ if (!fs.existsSync(migrationsPath)) {
111
+ fs.mkdirSync(migrationsPath);
112
+ }
113
+ const migrationPrefix = path.join(migrationsPath, `${timestamp()}_${args.name}`);
114
+ let wasmArgs = [
115
+ "migrations",
116
+ cidlPath,
117
+ `${migrationPrefix}.json`,
118
+ `${migrationPrefix}.sql`,
119
+ ];
120
+ // Add last migration if exists
121
+ {
122
+ const files = fs.readdirSync(migrationsPath);
123
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
124
+ // Sort descending by filename
125
+ jsonFiles.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
126
+ if (jsonFiles.length > 0) {
127
+ wasmArgs.push(path.join(migrationsPath, jsonFiles[0]));
128
+ }
129
+ }
130
+ const migrateConfig = {
131
+ name: "migrations",
132
+ wasmFile: "generator.wasm",
133
+ args: wasmArgs,
134
+ };
135
+ // Runs a generator command. Exits the process on failure.
136
+ await generate(migrateConfig);
137
+ },
138
+ }),
172
139
  },
173
140
  });
174
141
  async function extract(opts) {
@@ -177,7 +144,7 @@ async function extract(opts) {
177
144
  const config = loadCloesceConfig(projectRoot, opts.debug);
178
145
  const searchPaths = opts.inp ? [opts.inp] : (config.paths ?? [root]);
179
146
  const outputDir = config.outputDir ?? ".generated";
180
- const outPath = opts.out ?? path.join(outputDir, "cidl.json");
147
+ const outPath = opts.out ?? path.join(outputDir, "cidl.pre.json");
181
148
  const truncate = opts.truncateSourcePaths ?? config.truncateSourcePaths ?? false;
182
149
  const cloesceProjectName = opts.projectName ??
183
150
  config.projectName ??
@@ -205,6 +172,9 @@ async function extract(opts) {
205
172
  if (truncate) {
206
173
  ast.wrangler_env.source_path =
207
174
  "./" + path.basename(ast.wrangler_env.source_path);
175
+ if (ast.app_source) {
176
+ ast.app_source = "./" + path.basename(ast.app_source);
177
+ }
208
178
  for (const model of Object.values(ast.models)) {
209
179
  model.source_path =
210
180
  "./" + path.basename(model.source_path);
@@ -267,6 +237,16 @@ function loadCloesceConfig(root, debug = false) {
267
237
  }
268
238
  return {};
269
239
  }
240
+ function timestamp() {
241
+ const d = new Date();
242
+ return (d.getFullYear().toString() +
243
+ String(d.getMonth() + 1).padStart(2, "0") +
244
+ String(d.getDate()).padStart(2, "0") +
245
+ "T" +
246
+ String(d.getHours()).padStart(2, "0") +
247
+ String(d.getMinutes()).padStart(2, "0") +
248
+ String(d.getSeconds()).padStart(2, "0"));
249
+ }
270
250
  function readPackageJsonProjectName(cwd) {
271
251
  const pkgPath = path.join(cwd, "package.json");
272
252
  let projectName = path.basename(cwd);