@geekmidas/cli 10.0.0-alpha.28 → 10.0.0-alpha.29

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.
Files changed (45) hide show
  1. package/dist/{bundler-D5RcGmVM.mjs → bundler-7iaO93G8.mjs} +2 -2
  2. package/dist/{bundler-D5RcGmVM.mjs.map → bundler-7iaO93G8.mjs.map} +1 -1
  3. package/dist/{bundler-mjqGAvu1.cjs → bundler-CYF5mWi9.cjs} +2 -2
  4. package/dist/{bundler-mjqGAvu1.cjs.map → bundler-CYF5mWi9.cjs.map} +1 -1
  5. package/dist/config.d.cts +2 -2
  6. package/dist/config.d.mts +2 -2
  7. package/dist/{github-zkzu-ka2.mjs → github-DPpNfzu4.mjs} +2 -2
  8. package/dist/{github-zkzu-ka2.mjs.map → github-DPpNfzu4.mjs.map} +1 -1
  9. package/dist/{github-BUCXEl2v.cjs → github-ZL3RcUVD.cjs} +2 -2
  10. package/dist/{github-BUCXEl2v.cjs.map → github-ZL3RcUVD.cjs.map} +1 -1
  11. package/dist/{index-BPN7kghQ.d.cts → index-p89EzILa.d.mts} +2 -2
  12. package/dist/{index-BPN7kghQ.d.cts.map → index-p89EzILa.d.mts.map} +1 -1
  13. package/dist/{index-BqWKWRm0.d.mts → index-rU9u761a.d.cts} +2 -2
  14. package/dist/{index-BqWKWRm0.d.mts.map → index-rU9u761a.d.cts.map} +1 -1
  15. package/dist/index.cjs +189 -195
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.mjs +189 -195
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/openapi.d.cts +1 -1
  20. package/dist/openapi.d.mts +1 -1
  21. package/dist/reconcile/public.d.cts +1 -1
  22. package/dist/reconcile/public.d.mts +1 -1
  23. package/dist/{reconcile-szmJw_Sb.cjs → reconcile-BxZTV2vl.cjs} +5 -82
  24. package/dist/reconcile-BxZTV2vl.cjs.map +1 -0
  25. package/dist/{reconcile-DhbCvh4K.mjs → reconcile-yNq6Shau.mjs} +5 -82
  26. package/dist/reconcile-yNq6Shau.mjs.map +1 -0
  27. package/dist/{storage-TP0x118M.mjs → storage-BjTLjDDp.mjs} +1 -8
  28. package/dist/{storage-TP0x118M.mjs.map → storage-BjTLjDDp.mjs.map} +1 -1
  29. package/dist/{storage-DgypfSD6.cjs → storage-C2JC31V9.cjs} +1 -8
  30. package/dist/{storage-DgypfSD6.cjs.map → storage-C2JC31V9.cjs.map} +1 -1
  31. package/dist/{types-7R81hu0i.d.mts → types-CzK6hsXP.d.mts} +1 -17
  32. package/dist/types-CzK6hsXP.d.mts.map +1 -0
  33. package/dist/{types-N7c2O2m7.d.cts → types-DOOAevjG.d.cts} +1 -17
  34. package/dist/types-DOOAevjG.d.cts.map +1 -0
  35. package/dist/workspace/index.d.cts +2 -2
  36. package/dist/workspace/index.d.mts +2 -2
  37. package/dist/{workspace-Cv48QwPK.cjs → workspace-B3ApLW6p.cjs} +2 -2
  38. package/dist/{workspace-Cv48QwPK.cjs.map → workspace-B3ApLW6p.cjs.map} +1 -1
  39. package/dist/{workspace-cBobQVoA.mjs → workspace-DnHW8ooI.mjs} +2 -2
  40. package/dist/{workspace-cBobQVoA.mjs.map → workspace-DnHW8ooI.mjs.map} +1 -1
  41. package/package.json +14 -14
  42. package/dist/reconcile-DhbCvh4K.mjs.map +0 -1
  43. package/dist/reconcile-szmJw_Sb.cjs.map +0 -1
  44. package/dist/types-7R81hu0i.d.mts.map +0 -1
  45. package/dist/types-N7c2O2m7.d.cts.map +0 -1
package/dist/index.cjs CHANGED
@@ -4,11 +4,11 @@ const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
4
4
  const require_discover = require("./discover-BTh87DOO.cjs");
5
5
  const require_config = require("./config-BTFLHfRb.cjs");
6
6
  const require_workspace = require("./workspace-Bav3TVrP.cjs");
7
- const require_storage = require("./storage-DgypfSD6.cjs");
8
- const require_workspace$1 = require("./workspace-Cv48QwPK.cjs");
7
+ const require_storage = require("./storage-C2JC31V9.cjs");
8
+ const require_workspace$1 = require("./workspace-B3ApLW6p.cjs");
9
9
  const require_openapi = require("./openapi-K4H5tHzA.cjs");
10
10
  const require_emit = require("./emit-BdoPFjZf.cjs");
11
- const require_reconcile = require("./reconcile-szmJw_Sb.cjs");
11
+ const require_reconcile = require("./reconcile-BxZTV2vl.cjs");
12
12
  let commander = require("commander");
13
13
  let node_process = require("node:process");
14
14
  let node_readline_promises = require("node:readline/promises");
@@ -42,7 +42,7 @@ let node_dns_promises = require("node:dns/promises");
42
42
  let _geekmidas_envkit = require("@geekmidas/envkit");
43
43
  let _geekmidas_services = require("@geekmidas/services");
44
44
  //#region package.json
45
- var version = "10.0.0-alpha.28";
45
+ var version = "10.0.0-alpha.29";
46
46
  //#endregion
47
47
  //#region src/auth/credentials.ts
48
48
  /**
@@ -2906,7 +2906,7 @@ async function buildForProvider(provider, context, appRoot, discovered, endpoint
2906
2906
  let masterKey;
2907
2907
  if (context.production?.bundle && !skipBundle) {
2908
2908
  logger$8.log(`\n📦 Bundling production server...`);
2909
- const { bundleServer } = await Promise.resolve().then(() => require("./bundler-mjqGAvu1.cjs"));
2909
+ const { bundleServer } = await Promise.resolve().then(() => require("./bundler-CYF5mWi9.cjs"));
2910
2910
  const allConstructs = [
2911
2911
  ...endpoints.map((e) => e.construct),
2912
2912
  ...functions.map((f) => f.construct),
@@ -8132,15 +8132,20 @@ function databaseFiles() {
8132
8132
  return [{
8133
8133
  path: "src/constructs/database.ts",
8134
8134
  content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
8135
- import type { Generated } from 'kysely';
8135
+ import { CamelCasePlugin, type Generated } from 'kysely';
8136
8136
 
8137
- /** Your database schema. Add tables here. */
8137
+ /**
8138
+ * Your database schema. Add tables here.
8139
+ *
8140
+ * camelCase, because of the plugin below: the columns are snake_case in
8141
+ * Postgres (\`created_at\`), and Kysely maps between the two.
8142
+ */
8138
8143
  export interface Database {
8139
8144
  users: {
8140
8145
  id: Generated<string>;
8141
8146
  name: string;
8142
8147
  email: string;
8143
- created_at: Generated<Date>;
8148
+ createdAt: Generated<Date>;
8144
8149
  };
8145
8150
  }
8146
8151
 
@@ -8155,7 +8160,11 @@ export interface Database {
8155
8160
  * inference, so passing only \`Database\` would leave the name at \`string\`
8156
8161
  * and widen the service key away from \`${db.service}\`.
8157
8162
  */
8158
- export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
8163
+ export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}', {
8164
+ // Yours to keep, change or remove. Every connection this construct opens —
8165
+ // every endpoint, the tests — uses it.
8166
+ plugins: [new CamelCasePlugin()],
8167
+ });
8159
8168
  `
8160
8169
  }, usersMigration()];
8161
8170
  }
@@ -8330,7 +8339,7 @@ environment and fail in a way that looks like a config bug. Go through
8330
8339
  \`gkm exec --\`, which is what injects them.`);
8331
8340
  if (!isWorker) sections.push(endpointSection(options));
8332
8341
  if (isWorker || isServerless) sections.push(backgroundSection(template));
8333
- if (database) sections.push(databaseSection());
8342
+ if (database) sections.push(databaseSection(), queriesSection());
8334
8343
  sections.push(`## Environment
8335
8344
 
8336
8345
  Environment is parsed once, through \`envkit\`, and never read from
@@ -8517,6 +8526,88 @@ export const monthly = router
8517
8526
  Queries go through Kysely. Never open your own connection pool — the one the
8518
8527
  construct provides is the one that gets credentials, pooling and shutdown.`;
8519
8528
  }
8529
+ function queriesSection() {
8530
+ return `## Writing queries and relations
8531
+
8532
+ **Names: snake_case in Postgres, camelCase in TypeScript.** Kysely's
8533
+ \`CamelCasePlugin\` does the mapping, and it is set where you can see it — on
8534
+ the database construct in \`constructs/database.ts\`:
8535
+
8536
+ \`\`\`typescript
8537
+ export const database = new KyselyDatabase<Database, 'Database'>('Database', {
8538
+ plugins: [new CamelCasePlugin()],
8539
+ });
8540
+ \`\`\`
8541
+
8542
+ It is the project's plugin, not the toolbox's: keep it, change it, or remove it,
8543
+ but do it there. It applies to every connection the construct opens: every
8544
+ endpoint, and the tests, which connect through the construct.
8545
+
8546
+ - The \`Database\` interface is camelCase: \`userId\`, \`createdAt\`.
8547
+ - Migrations are the SQL, so they spell columns in snake_case.
8548
+ - A raw \`sql\` fragment is not rewritten. Refer to columns with
8549
+ \`sql.ref('users.createdAt')\` or \`eb.ref(…)\`, never inside the string.
8550
+ - A schema tenant takes its own options. Better Auth's tenant gets no plugin —
8551
+ its tables are its own.
8552
+
8553
+ **Types.** \`Generated<T>\` for every column the database fills in (ids,
8554
+ defaults, timestamps). Derive row types with \`Selectable<Database['users']>\`,
8555
+ \`Insertable<…>\` and \`Updateable<…>\` rather than writing them again.
8556
+
8557
+ **Relations are foreign keys.** Every \`*_id\` column references its parent,
8558
+ with its delete rule written out:
8559
+
8560
+ \`\`\`typescript
8561
+ .addColumn('user_id', 'uuid', (col) =>
8562
+ col.notNull().references('users.id').onDelete('cascade'),
8563
+ )
8564
+ \`\`\`
8565
+
8566
+ - \`restrict\` by default.
8567
+ - \`cascade\` only for rows that belong to the parent — a user's notifications.
8568
+ - \`set null\` for an optional link, on a nullable column.
8569
+ - Index every foreign key column. Postgres does not do it for you.
8570
+
8571
+ **Reading related rows: one query, nested.** \`jsonArrayFrom\` and
8572
+ \`jsonObjectFrom\` from \`kysely/helpers/postgres\` return children already in
8573
+ the shape of the response:
8574
+
8575
+ \`\`\`typescript
8576
+ import { jsonArrayFrom } from 'kysely/helpers/postgres';
8577
+
8578
+ const user = await db
8579
+ .selectFrom('users')
8580
+ .select((eb) => [
8581
+ 'users.id',
8582
+ 'users.name',
8583
+ jsonArrayFrom(
8584
+ eb
8585
+ .selectFrom('notifications')
8586
+ .select(['notifications.id', 'notifications.body', 'notifications.createdAt'])
8587
+ .whereRef('notifications.userId', '=', 'users.id')
8588
+ .orderBy('notifications.createdAt', 'desc'),
8589
+ ).as('notifications'),
8590
+ ])
8591
+ .where('users.id', '=', id)
8592
+ .executeTakeFirstOrThrow();
8593
+ \`\`\`
8594
+
8595
+ - Never query once per row in a loop.
8596
+ - Use joins to filter, or for a flat result — not to build nested ones by hand.
8597
+ - Qualify columns (\`users.id\`) once a query touches more than one table.
8598
+ - Select the columns the response needs. \`selectAll()\` only returns a whole
8599
+ row.
8600
+ - Nested rows come back as JSON: a timestamp inside \`jsonArrayFrom\` is a
8601
+ string, not a \`Date\`. Say so in the output schema (\`z.coerce.date()\`).
8602
+
8603
+ **Writes that touch several tables run in one transaction.** Use
8604
+ \`withTransaction(db, async (trx) => …)\` from \`@geekmidas/db/kysely\`: it joins
8605
+ the transaction \`db\` already is — an endpoint with an auditor, a test —
8606
+ instead of opening a second one.
8607
+
8608
+ **Lists paginate by cursor**, with \`paginatedSearch\` from
8609
+ \`@geekmidas/db/kysely/pagination\`, not by offset.`;
8610
+ }
8520
8611
  function conventionsSection() {
8521
8612
  return `## Conventions
8522
8613
 
@@ -8691,26 +8782,26 @@ function loadPackageJson() {
8691
8782
  * Run: pnpm --filter @geekmidas/cli sync-versions
8692
8783
  */
8693
8784
  const GEEKMIDAS_VERSIONS = {
8694
- "@geekmidas/audit": "~10.0.0-alpha.28",
8695
- "@geekmidas/auth": "~10.0.0-alpha.28",
8696
- "@geekmidas/cache": "~10.0.0-alpha.28",
8697
- "@geekmidas/client": "~10.0.0-alpha.28",
8698
- "@geekmidas/cloud": "~10.0.0-alpha.28",
8699
- "@geekmidas/constructs": "~10.0.0-alpha.28",
8700
- "@geekmidas/db": "~10.0.0-alpha.28",
8701
- "@geekmidas/emailkit": "~10.0.0-alpha.28",
8702
- "@geekmidas/envkit": "~10.0.0-alpha.28",
8703
- "@geekmidas/errors": "~10.0.0-alpha.28",
8704
- "@geekmidas/events": "~10.0.0-alpha.28",
8705
- "@geekmidas/logger": "~10.0.0-alpha.28",
8706
- "@geekmidas/manifest": "~10.0.0-alpha.28",
8707
- "@geekmidas/rate-limit": "~10.0.0-alpha.28",
8708
- "@geekmidas/schema": "~10.0.0-alpha.28",
8709
- "@geekmidas/services": "~10.0.0-alpha.28",
8710
- "@geekmidas/storage": "~10.0.0-alpha.28",
8711
- "@geekmidas/studio": "~10.0.0-alpha.28",
8712
- "@geekmidas/telescope": "~10.0.0-alpha.28",
8713
- "@geekmidas/testkit": "~10.0.0-alpha.28",
8785
+ "@geekmidas/audit": "~10.0.0-alpha.29",
8786
+ "@geekmidas/auth": "~10.0.0-alpha.29",
8787
+ "@geekmidas/cache": "~10.0.0-alpha.29",
8788
+ "@geekmidas/client": "~10.0.0-alpha.29",
8789
+ "@geekmidas/cloud": "~10.0.0-alpha.29",
8790
+ "@geekmidas/constructs": "~10.0.0-alpha.29",
8791
+ "@geekmidas/db": "~10.0.0-alpha.29",
8792
+ "@geekmidas/emailkit": "~10.0.0-alpha.29",
8793
+ "@geekmidas/envkit": "~10.0.0-alpha.29",
8794
+ "@geekmidas/errors": "~10.0.0-alpha.29",
8795
+ "@geekmidas/events": "~10.0.0-alpha.29",
8796
+ "@geekmidas/logger": "~10.0.0-alpha.29",
8797
+ "@geekmidas/manifest": "~10.0.0-alpha.29",
8798
+ "@geekmidas/rate-limit": "~10.0.0-alpha.29",
8799
+ "@geekmidas/schema": "~10.0.0-alpha.29",
8800
+ "@geekmidas/services": "~10.0.0-alpha.29",
8801
+ "@geekmidas/storage": "~10.0.0-alpha.29",
8802
+ "@geekmidas/studio": "~10.0.0-alpha.29",
8803
+ "@geekmidas/telescope": "~10.0.0-alpha.29",
8804
+ "@geekmidas/testkit": "~10.0.0-alpha.29",
8714
8805
  "@geekmidas/cli": `~${loadPackageJson().version}`
8715
8806
  };
8716
8807
  //#endregion
@@ -10648,15 +10739,20 @@ export const logger = createLogger();
10648
10739
  files.push({
10649
10740
  path: "constructs/database.ts",
10650
10741
  content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
10651
- import type { Generated } from 'kysely';
10742
+ import { CamelCasePlugin, type Generated } from 'kysely';
10652
10743
 
10653
- /** Your database schema. Add tables here. */
10744
+ /**
10745
+ * Your database schema. Add tables here.
10746
+ *
10747
+ * camelCase, because of the plugin below: the columns are snake_case in
10748
+ * Postgres (\`created_at\`), and Kysely maps between the two.
10749
+ */
10654
10750
  export interface Database {
10655
10751
  users: {
10656
10752
  id: Generated<string>;
10657
10753
  name: string;
10658
10754
  email: string;
10659
- created_at: Generated<Date>;
10755
+ createdAt: Generated<Date>;
10660
10756
  };
10661
10757
  }
10662
10758
 
@@ -10667,14 +10763,19 @@ export interface Database {
10667
10763
  * \`${db.urlKey}\` all derive from this line — which is why nothing lists
10668
10764
  * \`postgres\` anywhere.
10669
10765
  */
10670
- export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
10766
+ export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}', {
10767
+ // Yours to keep, change or remove. Every connection this construct opens —
10768
+ // every endpoint, the tests — uses it.
10769
+ plugins: [new CamelCasePlugin()],
10770
+ });
10671
10771
 
10672
10772
  /**
10673
10773
  * The auth server's own schema in that same Postgres, with its own role.
10674
10774
  *
10675
10775
  * A schema tenant rather than a second database: one container, and a role
10676
10776
  * whose \`search_path\` is pinned, so Better Auth's tables cannot collide with
10677
- * the application's.
10777
+ * the application's. It takes its own options: no plugin, because Better
10778
+ * Auth names its own columns.
10678
10779
  */
10679
10780
  export const authDb = database.schema<Record<string, never>, 'AuthDb'>('AuthDb');
10680
10781
  `
@@ -11002,54 +11103,11 @@ export const getUserEndpoint = router
11002
11103
  }
11003
11104
  ];
11004
11105
  if (options.monorepo) {
11005
- files.push({
11006
- path: "src/services/auth.ts",
11007
- content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
11008
-
11009
- export interface Session {
11010
- user: {
11011
- id: string;
11012
- email: string;
11013
- name: string;
11014
- };
11015
- }
11016
-
11017
- export interface AuthClient {
11018
- getSession: (cookie: string) => Promise<Session | null>;
11019
- }
11020
-
11021
- export const authService = {
11022
- serviceName: 'auth' as const,
11023
- async register({ envParser, context }: ServiceRegisterOptions) {
11024
- const logger = context.getLogger();
11025
-
11026
- const config = envParser
11027
- .create((get) => ({
11028
- url: get('AUTH_URL').string(),
11029
- }))
11030
- .parse();
11031
-
11032
- logger.info({ authUrl: config.url }, 'Auth service configured');
11033
-
11034
- return {
11035
- getSession: async (cookie: string): Promise<Session | null> => {
11036
- const res = await fetch(\`\${config.url}/api/auth/get-session\`, {
11037
- headers: { cookie },
11038
- });
11039
- if (!res.ok) return null;
11040
- return (await res.json()) as Session | null;
11041
- },
11042
- };
11043
- },
11044
- } satisfies Service<'auth', AuthClient>;
11045
- `
11046
- });
11047
11106
  files.push({
11048
11107
  path: "src/router.ts",
11049
11108
  content: `import { UnauthorizedError } from '@geekmidas/errors';
11050
11109
  import { api } from '${constructsImport("api")}';${options.constructs.database ? `
11051
11110
  import { database } from '${constructsImport("database")}';` : ""}
11052
- import { authService, type Session } from './services/auth.ts';
11053
11111
 
11054
11112
  /**
11055
11113
  * The shared endpoint factory — no session required.
@@ -11060,15 +11118,17 @@ import { authService, type Session } from './services/auth.ts';
11060
11118
  */
11061
11119
  export const router = api${options.constructs.database ? ".database(database)" : ""};
11062
11120
 
11063
- // The auth client available, but the session not enforced.
11064
- export const r = router.services([authService]);
11065
-
11066
- // Requires an active session — throws when there is none.
11067
- export const sessionRouter = r.session<Session>(async ({ services, header }) => {
11068
- const cookie = header('cookie') || '';
11069
- const session = await services.auth.getSession(cookie);
11121
+ /**
11122
+ * Requires an active session — throws when there is none.
11123
+ *
11124
+ * \`auth\` is the construct named in \`.auth()\` on the API, bound to this
11125
+ * request: it asks the auth server whose cookie this is. Handlers built from
11126
+ * this router get whatever this returns as \`session\`.
11127
+ */
11128
+ export const sessionRouter = router.session(async ({ auth }) => {
11129
+ const session = await auth.getSession();
11070
11130
 
11071
- if (!session?.user) {
11131
+ if (!session) {
11072
11132
  throw new UnauthorizedError('No active session');
11073
11133
  }
11074
11134
 
@@ -11155,46 +11215,6 @@ export const email = new Email('${mail.id}', { templates: {} });
11155
11215
  * schema and reached by its role, whatever the target.
11156
11216
  */
11157
11217
  export const cache = new Cache('${kv.id}');
11158
- `
11159
- });
11160
- if (options.constructs.database && !declares) files.push({
11161
- path: "src/services/database.ts",
11162
- content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
11163
- import { Kysely, PostgresDialect } from 'kysely';
11164
- import pg from 'pg';
11165
-
11166
- // Define your database schema
11167
- export interface Database {
11168
- users: {
11169
- id: string;
11170
- name: string;
11171
- email: string;
11172
- created_at: Date;
11173
- };
11174
- }
11175
-
11176
- export const databaseService = {
11177
- serviceName: 'database' as const,
11178
- async register({ envParser, context }: ServiceRegisterOptions) {
11179
- const logger = context.getLogger();
11180
- logger.info('Connecting to database');
11181
-
11182
- const config = envParser
11183
- .create((get) => ({
11184
- url: get('DATABASE_URL').string(),
11185
- }))
11186
- .parse();
11187
-
11188
- const db = new Kysely<Database>({
11189
- dialect: new PostgresDialect({
11190
- pool: new pg.Pool({ connectionString: config.url }),
11191
- }),
11192
- });
11193
-
11194
- logger.info('Database connection established');
11195
- return db;
11196
- },
11197
- } satisfies Service<'database', Kysely<Database>>;
11198
11218
  `
11199
11219
  });
11200
11220
  if (options.telescope) files.push({
@@ -11213,17 +11233,19 @@ export const telescope = new Telescope({
11213
11233
  content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
11214
11234
  import { Kysely, PostgresDialect } from 'kysely';
11215
11235
  import pg from 'pg';
11216
- import type { Database } from '${src(declares ? "constructs/database.ts" : "services/database.ts")}';
11236
+ import { type Database, database } from '${declares ? src("constructs/database.ts") : constructsImport("database")}';
11217
11237
  import { envParser } from '${src("config/env.ts")}';
11218
11238
 
11219
11239
  const studioConfig = envParser
11220
11240
  .create((get) => ({
11221
- databaseUrl: get('${declares ? db.urlKey : "DATABASE_URL"}').string(),
11241
+ databaseUrl: get('${db.urlKey}').string(),
11222
11242
  }))
11223
11243
  .parse();
11224
11244
 
11225
- // Create a Kysely instance for Studio
11245
+ // Studio's own client, built the way the construct builds its own — the same
11246
+ // plugins — so the browser shows what the app's queries see.
11226
11247
  const db = new Kysely<Database>({
11248
+ ...database.clientConfig,
11227
11249
  dialect: new PostgresDialect({
11228
11250
  pool: new pg.Pool({ connectionString: studioConfig.databaseUrl }),
11229
11251
  }),
@@ -11578,7 +11600,8 @@ const workerTemplate = {
11578
11600
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
11579
11601
  "@geekmidas/events": GEEKMIDAS_VERSIONS["@geekmidas/events"],
11580
11602
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
11581
- "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"]
11603
+ "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
11604
+ zod: DEPENDENCY_VERSIONS.zod
11582
11605
  },
11583
11606
  devDependencies: {
11584
11607
  "@biomejs/biome": DEPENDENCY_VERSIONS["@biomejs/biome"],
@@ -11637,51 +11660,34 @@ export const config = envParser
11637
11660
  content: loggerContent
11638
11661
  },
11639
11662
  {
11640
- path: "src/events/types.ts",
11641
- content: `import type { PublishableMessage } from '@geekmidas/events';
11642
-
11643
- // Define your event types here
11644
- export type AppEvents =
11645
- | PublishableMessage<'user.created', { userId: string; email: string }>
11646
- | PublishableMessage<'user.updated', { userId: string; changes: Record<string, unknown> }>
11647
- | PublishableMessage<'order.placed', { orderId: string; userId: string; total: number }>;
11648
- `
11649
- },
11650
- {
11651
- path: "src/events/publisher.ts",
11652
- content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
11653
- import { Publisher, type EventPublisher } from '@geekmidas/events';
11654
- import type { AppEvents } from './types.ts';
11655
-
11656
- export const eventsPublisherService = {
11657
- serviceName: 'events' as const,
11658
- async register({ envParser, context }: ServiceRegisterOptions) {
11659
- const logger = context.getLogger();
11660
- logger.info('Connecting to message broker');
11661
-
11662
- const config = envParser
11663
- .create((get) => ({
11664
- url: get('RABBITMQ_URL').string().default('amqp://localhost:5672'),
11665
- }))
11666
- .parse();
11667
-
11668
- const publisher = await Publisher.fromConnectionString<AppEvents>(
11669
- \`rabbitmq://\${config.url.replace('amqp://', '')}?exchange=events\`
11670
- );
11663
+ path: "src/constructs/topics.ts",
11664
+ content: `import { t } from '@geekmidas/constructs/topic';
11665
+ import { z } from 'zod';
11671
11666
 
11672
- logger.info('Message broker connection established');
11673
- return publisher;
11674
- },
11675
- } satisfies Service<'events', EventPublisher<AppEvents>>;
11667
+ /**
11668
+ * The \`users\` topic: pub/sub fan-out, any number of subscribers.
11669
+ *
11670
+ * The event map is the contract. It types \`users.publisher\` and every
11671
+ * subscriber that binds with \`.topic(users)\`, so an event cannot be published
11672
+ * in one shape and read in another.
11673
+ */
11674
+ export const users = t.topic('users').events({
11675
+ 'user.created': z.object({ userId: z.string(), email: z.email() }),
11676
+ 'user.updated': z.object({
11677
+ userId: z.string(),
11678
+ changes: z.record(z.string(), z.unknown()),
11679
+ }),
11680
+ });
11676
11681
  `
11677
11682
  },
11678
11683
  {
11679
11684
  path: "src/subscribers/user-events.ts",
11680
- content: `import { worker } from '~/constructs/worker.ts';
11681
- import { eventsPublisherService } from '~/events/publisher.ts';
11685
+ content: `import { users } from '~/constructs/topics.ts';
11686
+ import { worker } from '~/constructs/worker.ts';
11682
11687
 
11688
+ // Bound to the topic, not handed its publisher: a subscriber consumes.
11683
11689
  export const userEventsSubscriber = worker
11684
- .publisher(eventsPublisherService)
11690
+ .topic(users)
11685
11691
  .subscribe(['user.created', 'user.updated'])
11686
11692
  .handle(async ({ events, logger }) => {
11687
11693
  // A batch, not a single event. Both transports deliver in batches, so
@@ -11987,7 +11993,7 @@ function generateSourceFiles(options, template) {
11987
11993
  function generateTestFiles(options, _template) {
11988
11994
  if (!options.constructs.database) return [];
11989
11995
  const db = databaseFor();
11990
- const schema = !options.monorepo ? "../src/constructs/database.ts" : "~/services/database.ts";
11996
+ const schema = !options.monorepo ? "../src/constructs/database.ts" : `@${options.name}/constructs/database.ts`;
11991
11997
  const runtimeUrl = db.urlKey;
11992
11998
  const ownerUrl = db.ownerUrlKey;
11993
11999
  return [
@@ -12016,21 +12022,13 @@ export default defineConfig({
12016
12022
  {
12017
12023
  path: "test/config.ts",
12018
12024
  content: `import { it as itVitest } from 'vitest';
12019
- import { Kysely, PostgresDialect } from 'kysely';
12020
- import pg from 'pg';
12021
12025
  import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
12022
- import type { Database } from '${schema}';
12023
-
12024
- const db = new Kysely<Database>({
12025
- dialect: new PostgresDialect({
12026
- pool: new pg.Pool({ connectionString: process.env.${runtimeUrl} }),
12027
- }),
12028
- });
12026
+ import { type Database, database } from '${schema}';
12029
12027
 
12030
12028
  export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
12031
- // A function, or a construct: an instance is taken for a construct and has
12032
- // no service to register.
12033
- connection: () => db,
12029
+ // The construct itself, so the tests connect the way the app does — with
12030
+ // the plugins it was given, \`CamelCasePlugin\` included.
12031
+ connection: database,
12034
12032
  });
12035
12033
  `
12036
12034
  },
@@ -12104,7 +12102,7 @@ export const usersBuilder = KyselyFactory.createBuilder<Database, 'users'>(
12104
12102
  id: faker.string.uuid(),
12105
12103
  name: faker.person.fullName(),
12106
12104
  email: faker.internet.email(),
12107
- created_at: new Date(),
12105
+ createdAt: new Date(),
12108
12106
  }),
12109
12107
  );
12110
12108
  `
@@ -14817,7 +14815,6 @@ async function secretsInitCommand(options) {
14817
14815
  logger$2.log(` Location: .gkm/secrets/${stage}.json`);
14818
14816
  if (secrets.urls.DATABASE_URL) logger$2.log(`\n DATABASE_URL: ${maskUrl(secrets.urls.DATABASE_URL)}`);
14819
14817
  if (secrets.urls.REDIS_URL) logger$2.log(` REDIS_URL: ${maskUrl(secrets.urls.REDIS_URL)}`);
14820
- if (secrets.urls.RABBITMQ_URL) logger$2.log(` RABBITMQ_URL: ${maskUrl(secrets.urls.RABBITMQ_URL)}`);
14821
14818
  if (secrets.urls.STORAGE_ENDPOINT) logger$2.log(` STORAGE_ENDPOINT: ${secrets.urls.STORAGE_ENDPOINT}`);
14822
14819
  if (Object.keys(secrets.custom).length > 0) logger$2.log(`\n Custom secrets: ${Object.keys(secrets.custom).length}`);
14823
14820
  logger$2.log(`\n Use "gkm secrets:show --stage ${stage}" to view secrets`);
@@ -14879,13 +14876,11 @@ async function secretsShowCommand(options) {
14879
14876
  logger$2.log(` username: ${creds.username}`);
14880
14877
  logger$2.log(` password: ${reveal ? creds.password : require_storage.maskPassword(creds.password)}`);
14881
14878
  if (creds.database) logger$2.log(` database: ${creds.database}`);
14882
- if (creds.vhost) logger$2.log(` vhost: ${creds.vhost}`);
14883
14879
  if (creds.bucket) logger$2.log(` bucket: ${creds.bucket}`);
14884
14880
  }
14885
14881
  logger$2.log("\nConnection URLs:");
14886
14882
  if (secrets.urls.DATABASE_URL) logger$2.log(` DATABASE_URL: ${reveal ? secrets.urls.DATABASE_URL : maskUrl(secrets.urls.DATABASE_URL)}`);
14887
14883
  if (secrets.urls.REDIS_URL) logger$2.log(` REDIS_URL: ${reveal ? secrets.urls.REDIS_URL : maskUrl(secrets.urls.REDIS_URL)}`);
14888
- if (secrets.urls.RABBITMQ_URL) logger$2.log(` RABBITMQ_URL: ${reveal ? secrets.urls.RABBITMQ_URL : maskUrl(secrets.urls.RABBITMQ_URL)}`);
14889
14884
  if (secrets.urls.STORAGE_ENDPOINT) logger$2.log(` STORAGE_ENDPOINT: ${secrets.urls.STORAGE_ENDPOINT}`);
14890
14885
  if (Object.keys(secrets.custom).length > 0) {
14891
14886
  logger$2.log("\nCustom Secrets:");
@@ -15055,7 +15050,7 @@ var NoStoredSecrets = class extends Error {
15055
15050
  }
15056
15051
  };
15057
15052
  async function reconcile(secrets, workspace, stage) {
15058
- const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-Cv48QwPK.cjs")).then((n) => n.workspace_exports);
15053
+ const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-B3ApLW6p.cjs")).then((n) => n.workspace_exports);
15059
15054
  const result = require_reconcile.reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
15060
15055
  return result ? {
15061
15056
  secrets: result.secrets,
@@ -15215,8 +15210,7 @@ function credentialedServices(containers) {
15215
15210
  redis: "redis",
15216
15211
  minio: "minio",
15217
15212
  mailpit: "mailpit",
15218
- localstack: "localstack",
15219
- rabbitmq: "rabbitmq"
15213
+ localstack: "localstack"
15220
15214
  };
15221
15215
  return [...new Set(containers.map((container) => byContainer[container]).filter((name) => Boolean(name)))];
15222
15216
  }
@@ -15236,7 +15230,7 @@ function reconcileSecrets(secrets, workspace, containers = []) {
15236
15230
  [name]: creds
15237
15231
  }
15238
15232
  };
15239
- result.urls = require_reconcile.generateConnectionUrls(result.services, result.eventsBackend);
15233
+ result.urls = require_reconcile.generateConnectionUrls(result.services);
15240
15234
  logger$1.log(` 🔄 Adding missing service credentials: ${name}`);
15241
15235
  changed = true;
15242
15236
  }
@@ -15254,7 +15248,7 @@ function reconcileSecrets(secrets, workspace, containers = []) {
15254
15248
  }
15255
15249
  }
15256
15250
  };
15257
- result.urls = require_reconcile.generateConnectionUrls(result.services, result.eventsBackend);
15251
+ result.urls = require_reconcile.generateConnectionUrls(result.services);
15258
15252
  logger$1.log(" 🔄 Adding missing service credentials: pgboss");
15259
15253
  changed = true;
15260
15254
  }
@@ -16178,7 +16172,7 @@ program.command("secrets:show").description("Show secrets for a stage").required
16178
16172
  process.exit(1);
16179
16173
  }
16180
16174
  });
16181
- program.command("secrets:rotate").description("Rotate service passwords").requiredOption("--stage <stage>", "Stage name").option("--service <service>", "Specific service to rotate (postgres, redis, rabbitmq)").action(async (options) => {
16175
+ program.command("secrets:rotate").description("Rotate service passwords").requiredOption("--stage <stage>", "Stage name").option("--service <service>", "Specific service to rotate (postgres, redis, minio, …)").action(async (options) => {
16182
16176
  try {
16183
16177
  const globalOptions = program.opts();
16184
16178
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -16233,8 +16227,8 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16233
16227
  const globalOptions = program.opts();
16234
16228
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16235
16229
  const { loadWorkspaceConfig } = await Promise.resolve().then(() => require("./config.cjs"));
16236
- const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-szmJw_Sb.cjs")).then((n) => n.reconcile_exports);
16237
- const { readStageSecrets, writeStageSecrets } = await Promise.resolve().then(() => require("./storage-DgypfSD6.cjs")).then((n) => n.storage_exports);
16230
+ const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-BxZTV2vl.cjs")).then((n) => n.reconcile_exports);
16231
+ const { readStageSecrets, writeStageSecrets } = await Promise.resolve().then(() => require("./storage-C2JC31V9.cjs")).then((n) => n.storage_exports);
16238
16232
  const { workspace } = await loadWorkspaceConfig();
16239
16233
  const stage = options.stage ?? workspace.stages.local;
16240
16234
  const secrets = await readStageSecrets(stage, workspace.root);
@@ -16242,7 +16236,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16242
16236
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
16243
16237
  process.exit(1);
16244
16238
  }
16245
- const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-Cv48QwPK.cjs")).then((n) => n.workspace_exports);
16239
+ const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-B3ApLW6p.cjs")).then((n) => n.workspace_exports);
16246
16240
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
16247
16241
  if (!result) {
16248
16242
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
@@ -16301,7 +16295,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
16301
16295
  try {
16302
16296
  const globalOptions = program.opts();
16303
16297
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16304
- const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-BUCXEl2v.cjs"));
16298
+ const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-ZL3RcUVD.cjs"));
16305
16299
  await deployGithubCommand(options);
16306
16300
  } catch (error) {
16307
16301
  console.error(formatError(error));