@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.mjs CHANGED
@@ -3,11 +3,11 @@ import { n as __require, t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mj
3
3
  import { i as DEFAULT_CACHE, r as ConstructGenerator, s as isPartitionedRoutes, t as discover } from "./discover-CXGlQHm1.mjs";
4
4
  import { a as loadConfig, c as parseModuleConfig, i as loadAppConfig, l as appKey, r as getAppNameFromCwd, s as loadWorkspaceConfig, u as derivedApps } from "./config-Dqxa-_Ge.mjs";
5
5
  import { D as assertDeployedStage, E as TEST_STAGE, O as deployedProblems, T as InvalidStages, a as getAppBuildOrder, b as getDeployTargetError, k as stageProblems, m as stripPublicPrefix, o as getAppGkmConfig, p as getPublicEnvPrefix, t as allConstructGlobs, x as isDeployTargetSupported } from "./workspace-T4IVGSoB.mjs";
6
- import { c as getKeyPath, i as setCustomSecret, n as readStageSecrets, o as toEmbeddableSecrets, r as secretsExist, s as writeStageSecrets, t as maskPassword } from "./storage-TP0x118M.mjs";
7
- import { _ as findAvailablePort, b as loadSecretsForApp, c as applyPostgres, d as cacheBackendFor, f as eventsBackendFor, g as createEntryWrapper, h as createCredentialsPreload, i as reconcileWorkspace, l as resourceName, m as storageBackendFor, n as constructGlobs, o as COMPOSE_PATH, p as providerOf, r as derivedContainers, s as LOCAL_CA_PATH, t as backendsOf, u as rootSite, v as isPortAvailable, x as prepareEntryCredentials, y as loadEnvFiles } from "./workspace-cBobQVoA.mjs";
6
+ import { c as getKeyPath, i as setCustomSecret, n as readStageSecrets, o as toEmbeddableSecrets, r as secretsExist, s as writeStageSecrets, t as maskPassword } from "./storage-BjTLjDDp.mjs";
7
+ import { _ as findAvailablePort, b as loadSecretsForApp, c as applyPostgres, d as cacheBackendFor, f as eventsBackendFor, g as createEntryWrapper, h as createCredentialsPreload, i as reconcileWorkspace, l as resourceName, m as storageBackendFor, n as constructGlobs, o as COMPOSE_PATH, p as providerOf, r as derivedContainers, s as LOCAL_CA_PATH, t as backendsOf, u as rootSite, v as isPortAvailable, x as prepareEntryCredentials, y as loadEnvFiles } from "./workspace-DnHW8ooI.mjs";
8
8
  import { a as openapiCommand, c as EndpointGenerator, l as runtimeFor, n as generateOpenApi, o as resolveOpenApiConfig, s as OpenApiTsGenerator } from "./openapi-D-Lk5Hr5.mjs";
9
9
  import { i as withRoutes, r as withCompute } from "./emit-Bzq7JsMP.mjs";
10
- import { a as createStageSecrets, c as generateServiceCredentials, i as writeDockerEnvFromSecrets, l as rotateServicePassword, o as generateConnectionUrls, r as generateFullstackCustomSecrets, s as generateSecurePassword, t as reconcileMissingSecrets } from "./reconcile-DhbCvh4K.mjs";
10
+ import { a as createStageSecrets, c as generateServiceCredentials, i as writeDockerEnvFromSecrets, l as rotateServicePassword, o as generateConnectionUrls, r as generateFullstackCustomSecrets, s as generateSecurePassword, t as reconcileMissingSecrets } from "./reconcile-yNq6Shau.mjs";
11
11
  import { createRequire } from "node:module";
12
12
  import { Command } from "commander";
13
13
  import { stdin, stdout } from "node:process";
@@ -37,7 +37,7 @@ import { lookup } from "node:dns/promises";
37
37
  import { EnvironmentParser } from "@geekmidas/envkit";
38
38
  import { serviceContext } from "@geekmidas/services";
39
39
  //#region package.json
40
- var version = "10.0.0-alpha.28";
40
+ var version = "10.0.0-alpha.29";
41
41
  //#endregion
42
42
  //#region src/auth/credentials.ts
43
43
  /**
@@ -2901,7 +2901,7 @@ async function buildForProvider(provider, context, appRoot, discovered, endpoint
2901
2901
  let masterKey;
2902
2902
  if (context.production?.bundle && !skipBundle) {
2903
2903
  logger$8.log(`\n📦 Bundling production server...`);
2904
- const { bundleServer } = await import("./bundler-D5RcGmVM.mjs");
2904
+ const { bundleServer } = await import("./bundler-7iaO93G8.mjs");
2905
2905
  const allConstructs = [
2906
2906
  ...endpoints.map((e) => e.construct),
2907
2907
  ...functions.map((f) => f.construct),
@@ -8121,15 +8121,20 @@ function databaseFiles() {
8121
8121
  return [{
8122
8122
  path: "src/constructs/database.ts",
8123
8123
  content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
8124
- import type { Generated } from 'kysely';
8124
+ import { CamelCasePlugin, type Generated } from 'kysely';
8125
8125
 
8126
- /** Your database schema. Add tables here. */
8126
+ /**
8127
+ * Your database schema. Add tables here.
8128
+ *
8129
+ * camelCase, because of the plugin below: the columns are snake_case in
8130
+ * Postgres (\`created_at\`), and Kysely maps between the two.
8131
+ */
8127
8132
  export interface Database {
8128
8133
  users: {
8129
8134
  id: Generated<string>;
8130
8135
  name: string;
8131
8136
  email: string;
8132
- created_at: Generated<Date>;
8137
+ createdAt: Generated<Date>;
8133
8138
  };
8134
8139
  }
8135
8140
 
@@ -8144,7 +8149,11 @@ export interface Database {
8144
8149
  * inference, so passing only \`Database\` would leave the name at \`string\`
8145
8150
  * and widen the service key away from \`${db.service}\`.
8146
8151
  */
8147
- export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
8152
+ export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}', {
8153
+ // Yours to keep, change or remove. Every connection this construct opens —
8154
+ // every endpoint, the tests — uses it.
8155
+ plugins: [new CamelCasePlugin()],
8156
+ });
8148
8157
  `
8149
8158
  }, usersMigration()];
8150
8159
  }
@@ -8319,7 +8328,7 @@ environment and fail in a way that looks like a config bug. Go through
8319
8328
  \`gkm exec --\`, which is what injects them.`);
8320
8329
  if (!isWorker) sections.push(endpointSection(options));
8321
8330
  if (isWorker || isServerless) sections.push(backgroundSection(template));
8322
- if (database) sections.push(databaseSection());
8331
+ if (database) sections.push(databaseSection(), queriesSection());
8323
8332
  sections.push(`## Environment
8324
8333
 
8325
8334
  Environment is parsed once, through \`envkit\`, and never read from
@@ -8506,6 +8515,88 @@ export const monthly = router
8506
8515
  Queries go through Kysely. Never open your own connection pool — the one the
8507
8516
  construct provides is the one that gets credentials, pooling and shutdown.`;
8508
8517
  }
8518
+ function queriesSection() {
8519
+ return `## Writing queries and relations
8520
+
8521
+ **Names: snake_case in Postgres, camelCase in TypeScript.** Kysely's
8522
+ \`CamelCasePlugin\` does the mapping, and it is set where you can see it — on
8523
+ the database construct in \`constructs/database.ts\`:
8524
+
8525
+ \`\`\`typescript
8526
+ export const database = new KyselyDatabase<Database, 'Database'>('Database', {
8527
+ plugins: [new CamelCasePlugin()],
8528
+ });
8529
+ \`\`\`
8530
+
8531
+ It is the project's plugin, not the toolbox's: keep it, change it, or remove it,
8532
+ but do it there. It applies to every connection the construct opens: every
8533
+ endpoint, and the tests, which connect through the construct.
8534
+
8535
+ - The \`Database\` interface is camelCase: \`userId\`, \`createdAt\`.
8536
+ - Migrations are the SQL, so they spell columns in snake_case.
8537
+ - A raw \`sql\` fragment is not rewritten. Refer to columns with
8538
+ \`sql.ref('users.createdAt')\` or \`eb.ref(…)\`, never inside the string.
8539
+ - A schema tenant takes its own options. Better Auth's tenant gets no plugin —
8540
+ its tables are its own.
8541
+
8542
+ **Types.** \`Generated<T>\` for every column the database fills in (ids,
8543
+ defaults, timestamps). Derive row types with \`Selectable<Database['users']>\`,
8544
+ \`Insertable<…>\` and \`Updateable<…>\` rather than writing them again.
8545
+
8546
+ **Relations are foreign keys.** Every \`*_id\` column references its parent,
8547
+ with its delete rule written out:
8548
+
8549
+ \`\`\`typescript
8550
+ .addColumn('user_id', 'uuid', (col) =>
8551
+ col.notNull().references('users.id').onDelete('cascade'),
8552
+ )
8553
+ \`\`\`
8554
+
8555
+ - \`restrict\` by default.
8556
+ - \`cascade\` only for rows that belong to the parent — a user's notifications.
8557
+ - \`set null\` for an optional link, on a nullable column.
8558
+ - Index every foreign key column. Postgres does not do it for you.
8559
+
8560
+ **Reading related rows: one query, nested.** \`jsonArrayFrom\` and
8561
+ \`jsonObjectFrom\` from \`kysely/helpers/postgres\` return children already in
8562
+ the shape of the response:
8563
+
8564
+ \`\`\`typescript
8565
+ import { jsonArrayFrom } from 'kysely/helpers/postgres';
8566
+
8567
+ const user = await db
8568
+ .selectFrom('users')
8569
+ .select((eb) => [
8570
+ 'users.id',
8571
+ 'users.name',
8572
+ jsonArrayFrom(
8573
+ eb
8574
+ .selectFrom('notifications')
8575
+ .select(['notifications.id', 'notifications.body', 'notifications.createdAt'])
8576
+ .whereRef('notifications.userId', '=', 'users.id')
8577
+ .orderBy('notifications.createdAt', 'desc'),
8578
+ ).as('notifications'),
8579
+ ])
8580
+ .where('users.id', '=', id)
8581
+ .executeTakeFirstOrThrow();
8582
+ \`\`\`
8583
+
8584
+ - Never query once per row in a loop.
8585
+ - Use joins to filter, or for a flat result — not to build nested ones by hand.
8586
+ - Qualify columns (\`users.id\`) once a query touches more than one table.
8587
+ - Select the columns the response needs. \`selectAll()\` only returns a whole
8588
+ row.
8589
+ - Nested rows come back as JSON: a timestamp inside \`jsonArrayFrom\` is a
8590
+ string, not a \`Date\`. Say so in the output schema (\`z.coerce.date()\`).
8591
+
8592
+ **Writes that touch several tables run in one transaction.** Use
8593
+ \`withTransaction(db, async (trx) => …)\` from \`@geekmidas/db/kysely\`: it joins
8594
+ the transaction \`db\` already is — an endpoint with an auditor, a test —
8595
+ instead of opening a second one.
8596
+
8597
+ **Lists paginate by cursor**, with \`paginatedSearch\` from
8598
+ \`@geekmidas/db/kysely/pagination\`, not by offset.`;
8599
+ }
8509
8600
  function conventionsSection() {
8510
8601
  return `## Conventions
8511
8602
 
@@ -8680,26 +8771,26 @@ function loadPackageJson() {
8680
8771
  * Run: pnpm --filter @geekmidas/cli sync-versions
8681
8772
  */
8682
8773
  const GEEKMIDAS_VERSIONS = {
8683
- "@geekmidas/audit": "~10.0.0-alpha.28",
8684
- "@geekmidas/auth": "~10.0.0-alpha.28",
8685
- "@geekmidas/cache": "~10.0.0-alpha.28",
8686
- "@geekmidas/client": "~10.0.0-alpha.28",
8687
- "@geekmidas/cloud": "~10.0.0-alpha.28",
8688
- "@geekmidas/constructs": "~10.0.0-alpha.28",
8689
- "@geekmidas/db": "~10.0.0-alpha.28",
8690
- "@geekmidas/emailkit": "~10.0.0-alpha.28",
8691
- "@geekmidas/envkit": "~10.0.0-alpha.28",
8692
- "@geekmidas/errors": "~10.0.0-alpha.28",
8693
- "@geekmidas/events": "~10.0.0-alpha.28",
8694
- "@geekmidas/logger": "~10.0.0-alpha.28",
8695
- "@geekmidas/manifest": "~10.0.0-alpha.28",
8696
- "@geekmidas/rate-limit": "~10.0.0-alpha.28",
8697
- "@geekmidas/schema": "~10.0.0-alpha.28",
8698
- "@geekmidas/services": "~10.0.0-alpha.28",
8699
- "@geekmidas/storage": "~10.0.0-alpha.28",
8700
- "@geekmidas/studio": "~10.0.0-alpha.28",
8701
- "@geekmidas/telescope": "~10.0.0-alpha.28",
8702
- "@geekmidas/testkit": "~10.0.0-alpha.28",
8774
+ "@geekmidas/audit": "~10.0.0-alpha.29",
8775
+ "@geekmidas/auth": "~10.0.0-alpha.29",
8776
+ "@geekmidas/cache": "~10.0.0-alpha.29",
8777
+ "@geekmidas/client": "~10.0.0-alpha.29",
8778
+ "@geekmidas/cloud": "~10.0.0-alpha.29",
8779
+ "@geekmidas/constructs": "~10.0.0-alpha.29",
8780
+ "@geekmidas/db": "~10.0.0-alpha.29",
8781
+ "@geekmidas/emailkit": "~10.0.0-alpha.29",
8782
+ "@geekmidas/envkit": "~10.0.0-alpha.29",
8783
+ "@geekmidas/errors": "~10.0.0-alpha.29",
8784
+ "@geekmidas/events": "~10.0.0-alpha.29",
8785
+ "@geekmidas/logger": "~10.0.0-alpha.29",
8786
+ "@geekmidas/manifest": "~10.0.0-alpha.29",
8787
+ "@geekmidas/rate-limit": "~10.0.0-alpha.29",
8788
+ "@geekmidas/schema": "~10.0.0-alpha.29",
8789
+ "@geekmidas/services": "~10.0.0-alpha.29",
8790
+ "@geekmidas/storage": "~10.0.0-alpha.29",
8791
+ "@geekmidas/studio": "~10.0.0-alpha.29",
8792
+ "@geekmidas/telescope": "~10.0.0-alpha.29",
8793
+ "@geekmidas/testkit": "~10.0.0-alpha.29",
8703
8794
  "@geekmidas/cli": `~${loadPackageJson().version}`
8704
8795
  };
8705
8796
  //#endregion
@@ -10637,15 +10728,20 @@ export const logger = createLogger();
10637
10728
  files.push({
10638
10729
  path: "constructs/database.ts",
10639
10730
  content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
10640
- import type { Generated } from 'kysely';
10731
+ import { CamelCasePlugin, type Generated } from 'kysely';
10641
10732
 
10642
- /** Your database schema. Add tables here. */
10733
+ /**
10734
+ * Your database schema. Add tables here.
10735
+ *
10736
+ * camelCase, because of the plugin below: the columns are snake_case in
10737
+ * Postgres (\`created_at\`), and Kysely maps between the two.
10738
+ */
10643
10739
  export interface Database {
10644
10740
  users: {
10645
10741
  id: Generated<string>;
10646
10742
  name: string;
10647
10743
  email: string;
10648
- created_at: Generated<Date>;
10744
+ createdAt: Generated<Date>;
10649
10745
  };
10650
10746
  }
10651
10747
 
@@ -10656,14 +10752,19 @@ export interface Database {
10656
10752
  * \`${db.urlKey}\` all derive from this line — which is why nothing lists
10657
10753
  * \`postgres\` anywhere.
10658
10754
  */
10659
- export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
10755
+ export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}', {
10756
+ // Yours to keep, change or remove. Every connection this construct opens —
10757
+ // every endpoint, the tests — uses it.
10758
+ plugins: [new CamelCasePlugin()],
10759
+ });
10660
10760
 
10661
10761
  /**
10662
10762
  * The auth server's own schema in that same Postgres, with its own role.
10663
10763
  *
10664
10764
  * A schema tenant rather than a second database: one container, and a role
10665
10765
  * whose \`search_path\` is pinned, so Better Auth's tables cannot collide with
10666
- * the application's.
10766
+ * the application's. It takes its own options: no plugin, because Better
10767
+ * Auth names its own columns.
10667
10768
  */
10668
10769
  export const authDb = database.schema<Record<string, never>, 'AuthDb'>('AuthDb');
10669
10770
  `
@@ -10991,54 +11092,11 @@ export const getUserEndpoint = router
10991
11092
  }
10992
11093
  ];
10993
11094
  if (options.monorepo) {
10994
- files.push({
10995
- path: "src/services/auth.ts",
10996
- content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
10997
-
10998
- export interface Session {
10999
- user: {
11000
- id: string;
11001
- email: string;
11002
- name: string;
11003
- };
11004
- }
11005
-
11006
- export interface AuthClient {
11007
- getSession: (cookie: string) => Promise<Session | null>;
11008
- }
11009
-
11010
- export const authService = {
11011
- serviceName: 'auth' as const,
11012
- async register({ envParser, context }: ServiceRegisterOptions) {
11013
- const logger = context.getLogger();
11014
-
11015
- const config = envParser
11016
- .create((get) => ({
11017
- url: get('AUTH_URL').string(),
11018
- }))
11019
- .parse();
11020
-
11021
- logger.info({ authUrl: config.url }, 'Auth service configured');
11022
-
11023
- return {
11024
- getSession: async (cookie: string): Promise<Session | null> => {
11025
- const res = await fetch(\`\${config.url}/api/auth/get-session\`, {
11026
- headers: { cookie },
11027
- });
11028
- if (!res.ok) return null;
11029
- return (await res.json()) as Session | null;
11030
- },
11031
- };
11032
- },
11033
- } satisfies Service<'auth', AuthClient>;
11034
- `
11035
- });
11036
11095
  files.push({
11037
11096
  path: "src/router.ts",
11038
11097
  content: `import { UnauthorizedError } from '@geekmidas/errors';
11039
11098
  import { api } from '${constructsImport("api")}';${options.constructs.database ? `
11040
11099
  import { database } from '${constructsImport("database")}';` : ""}
11041
- import { authService, type Session } from './services/auth.ts';
11042
11100
 
11043
11101
  /**
11044
11102
  * The shared endpoint factory — no session required.
@@ -11049,15 +11107,17 @@ import { authService, type Session } from './services/auth.ts';
11049
11107
  */
11050
11108
  export const router = api${options.constructs.database ? ".database(database)" : ""};
11051
11109
 
11052
- // The auth client available, but the session not enforced.
11053
- export const r = router.services([authService]);
11054
-
11055
- // Requires an active session — throws when there is none.
11056
- export const sessionRouter = r.session<Session>(async ({ services, header }) => {
11057
- const cookie = header('cookie') || '';
11058
- const session = await services.auth.getSession(cookie);
11110
+ /**
11111
+ * Requires an active session — throws when there is none.
11112
+ *
11113
+ * \`auth\` is the construct named in \`.auth()\` on the API, bound to this
11114
+ * request: it asks the auth server whose cookie this is. Handlers built from
11115
+ * this router get whatever this returns as \`session\`.
11116
+ */
11117
+ export const sessionRouter = router.session(async ({ auth }) => {
11118
+ const session = await auth.getSession();
11059
11119
 
11060
- if (!session?.user) {
11120
+ if (!session) {
11061
11121
  throw new UnauthorizedError('No active session');
11062
11122
  }
11063
11123
 
@@ -11144,46 +11204,6 @@ export const email = new Email('${mail.id}', { templates: {} });
11144
11204
  * schema and reached by its role, whatever the target.
11145
11205
  */
11146
11206
  export const cache = new Cache('${kv.id}');
11147
- `
11148
- });
11149
- if (options.constructs.database && !declares) files.push({
11150
- path: "src/services/database.ts",
11151
- content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
11152
- import { Kysely, PostgresDialect } from 'kysely';
11153
- import pg from 'pg';
11154
-
11155
- // Define your database schema
11156
- export interface Database {
11157
- users: {
11158
- id: string;
11159
- name: string;
11160
- email: string;
11161
- created_at: Date;
11162
- };
11163
- }
11164
-
11165
- export const databaseService = {
11166
- serviceName: 'database' as const,
11167
- async register({ envParser, context }: ServiceRegisterOptions) {
11168
- const logger = context.getLogger();
11169
- logger.info('Connecting to database');
11170
-
11171
- const config = envParser
11172
- .create((get) => ({
11173
- url: get('DATABASE_URL').string(),
11174
- }))
11175
- .parse();
11176
-
11177
- const db = new Kysely<Database>({
11178
- dialect: new PostgresDialect({
11179
- pool: new pg.Pool({ connectionString: config.url }),
11180
- }),
11181
- });
11182
-
11183
- logger.info('Database connection established');
11184
- return db;
11185
- },
11186
- } satisfies Service<'database', Kysely<Database>>;
11187
11207
  `
11188
11208
  });
11189
11209
  if (options.telescope) files.push({
@@ -11202,17 +11222,19 @@ export const telescope = new Telescope({
11202
11222
  content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
11203
11223
  import { Kysely, PostgresDialect } from 'kysely';
11204
11224
  import pg from 'pg';
11205
- import type { Database } from '${src(declares ? "constructs/database.ts" : "services/database.ts")}';
11225
+ import { type Database, database } from '${declares ? src("constructs/database.ts") : constructsImport("database")}';
11206
11226
  import { envParser } from '${src("config/env.ts")}';
11207
11227
 
11208
11228
  const studioConfig = envParser
11209
11229
  .create((get) => ({
11210
- databaseUrl: get('${declares ? db.urlKey : "DATABASE_URL"}').string(),
11230
+ databaseUrl: get('${db.urlKey}').string(),
11211
11231
  }))
11212
11232
  .parse();
11213
11233
 
11214
- // Create a Kysely instance for Studio
11234
+ // Studio's own client, built the way the construct builds its own — the same
11235
+ // plugins — so the browser shows what the app's queries see.
11215
11236
  const db = new Kysely<Database>({
11237
+ ...database.clientConfig,
11216
11238
  dialect: new PostgresDialect({
11217
11239
  pool: new pg.Pool({ connectionString: studioConfig.databaseUrl }),
11218
11240
  }),
@@ -11567,7 +11589,8 @@ const workerTemplate = {
11567
11589
  "@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
11568
11590
  "@geekmidas/events": GEEKMIDAS_VERSIONS["@geekmidas/events"],
11569
11591
  "@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
11570
- "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"]
11592
+ "@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
11593
+ zod: DEPENDENCY_VERSIONS.zod
11571
11594
  },
11572
11595
  devDependencies: {
11573
11596
  "@biomejs/biome": DEPENDENCY_VERSIONS["@biomejs/biome"],
@@ -11626,51 +11649,34 @@ export const config = envParser
11626
11649
  content: loggerContent
11627
11650
  },
11628
11651
  {
11629
- path: "src/events/types.ts",
11630
- content: `import type { PublishableMessage } from '@geekmidas/events';
11631
-
11632
- // Define your event types here
11633
- export type AppEvents =
11634
- | PublishableMessage<'user.created', { userId: string; email: string }>
11635
- | PublishableMessage<'user.updated', { userId: string; changes: Record<string, unknown> }>
11636
- | PublishableMessage<'order.placed', { orderId: string; userId: string; total: number }>;
11637
- `
11638
- },
11639
- {
11640
- path: "src/events/publisher.ts",
11641
- content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
11642
- import { Publisher, type EventPublisher } from '@geekmidas/events';
11643
- import type { AppEvents } from './types.ts';
11644
-
11645
- export const eventsPublisherService = {
11646
- serviceName: 'events' as const,
11647
- async register({ envParser, context }: ServiceRegisterOptions) {
11648
- const logger = context.getLogger();
11649
- logger.info('Connecting to message broker');
11650
-
11651
- const config = envParser
11652
- .create((get) => ({
11653
- url: get('RABBITMQ_URL').string().default('amqp://localhost:5672'),
11654
- }))
11655
- .parse();
11656
-
11657
- const publisher = await Publisher.fromConnectionString<AppEvents>(
11658
- \`rabbitmq://\${config.url.replace('amqp://', '')}?exchange=events\`
11659
- );
11652
+ path: "src/constructs/topics.ts",
11653
+ content: `import { t } from '@geekmidas/constructs/topic';
11654
+ import { z } from 'zod';
11660
11655
 
11661
- logger.info('Message broker connection established');
11662
- return publisher;
11663
- },
11664
- } satisfies Service<'events', EventPublisher<AppEvents>>;
11656
+ /**
11657
+ * The \`users\` topic: pub/sub fan-out, any number of subscribers.
11658
+ *
11659
+ * The event map is the contract. It types \`users.publisher\` and every
11660
+ * subscriber that binds with \`.topic(users)\`, so an event cannot be published
11661
+ * in one shape and read in another.
11662
+ */
11663
+ export const users = t.topic('users').events({
11664
+ 'user.created': z.object({ userId: z.string(), email: z.email() }),
11665
+ 'user.updated': z.object({
11666
+ userId: z.string(),
11667
+ changes: z.record(z.string(), z.unknown()),
11668
+ }),
11669
+ });
11665
11670
  `
11666
11671
  },
11667
11672
  {
11668
11673
  path: "src/subscribers/user-events.ts",
11669
- content: `import { worker } from '~/constructs/worker.ts';
11670
- import { eventsPublisherService } from '~/events/publisher.ts';
11674
+ content: `import { users } from '~/constructs/topics.ts';
11675
+ import { worker } from '~/constructs/worker.ts';
11671
11676
 
11677
+ // Bound to the topic, not handed its publisher: a subscriber consumes.
11672
11678
  export const userEventsSubscriber = worker
11673
- .publisher(eventsPublisherService)
11679
+ .topic(users)
11674
11680
  .subscribe(['user.created', 'user.updated'])
11675
11681
  .handle(async ({ events, logger }) => {
11676
11682
  // A batch, not a single event. Both transports deliver in batches, so
@@ -11976,7 +11982,7 @@ function generateSourceFiles(options, template) {
11976
11982
  function generateTestFiles(options, _template) {
11977
11983
  if (!options.constructs.database) return [];
11978
11984
  const db = databaseFor();
11979
- const schema = !options.monorepo ? "../src/constructs/database.ts" : "~/services/database.ts";
11985
+ const schema = !options.monorepo ? "../src/constructs/database.ts" : `@${options.name}/constructs/database.ts`;
11980
11986
  const runtimeUrl = db.urlKey;
11981
11987
  const ownerUrl = db.ownerUrlKey;
11982
11988
  return [
@@ -12005,21 +12011,13 @@ export default defineConfig({
12005
12011
  {
12006
12012
  path: "test/config.ts",
12007
12013
  content: `import { it as itVitest } from 'vitest';
12008
- import { Kysely, PostgresDialect } from 'kysely';
12009
- import pg from 'pg';
12010
12014
  import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
12011
- import type { Database } from '${schema}';
12012
-
12013
- const db = new Kysely<Database>({
12014
- dialect: new PostgresDialect({
12015
- pool: new pg.Pool({ connectionString: process.env.${runtimeUrl} }),
12016
- }),
12017
- });
12015
+ import { type Database, database } from '${schema}';
12018
12016
 
12019
12017
  export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
12020
- // A function, or a construct: an instance is taken for a construct and has
12021
- // no service to register.
12022
- connection: () => db,
12018
+ // The construct itself, so the tests connect the way the app does — with
12019
+ // the plugins it was given, \`CamelCasePlugin\` included.
12020
+ connection: database,
12023
12021
  });
12024
12022
  `
12025
12023
  },
@@ -12093,7 +12091,7 @@ export const usersBuilder = KyselyFactory.createBuilder<Database, 'users'>(
12093
12091
  id: faker.string.uuid(),
12094
12092
  name: faker.person.fullName(),
12095
12093
  email: faker.internet.email(),
12096
- created_at: new Date(),
12094
+ createdAt: new Date(),
12097
12095
  }),
12098
12096
  );
12099
12097
  `
@@ -14806,7 +14804,6 @@ async function secretsInitCommand(options) {
14806
14804
  logger$2.log(` Location: .gkm/secrets/${stage}.json`);
14807
14805
  if (secrets.urls.DATABASE_URL) logger$2.log(`\n DATABASE_URL: ${maskUrl(secrets.urls.DATABASE_URL)}`);
14808
14806
  if (secrets.urls.REDIS_URL) logger$2.log(` REDIS_URL: ${maskUrl(secrets.urls.REDIS_URL)}`);
14809
- if (secrets.urls.RABBITMQ_URL) logger$2.log(` RABBITMQ_URL: ${maskUrl(secrets.urls.RABBITMQ_URL)}`);
14810
14807
  if (secrets.urls.STORAGE_ENDPOINT) logger$2.log(` STORAGE_ENDPOINT: ${secrets.urls.STORAGE_ENDPOINT}`);
14811
14808
  if (Object.keys(secrets.custom).length > 0) logger$2.log(`\n Custom secrets: ${Object.keys(secrets.custom).length}`);
14812
14809
  logger$2.log(`\n Use "gkm secrets:show --stage ${stage}" to view secrets`);
@@ -14868,13 +14865,11 @@ async function secretsShowCommand(options) {
14868
14865
  logger$2.log(` username: ${creds.username}`);
14869
14866
  logger$2.log(` password: ${reveal ? creds.password : maskPassword(creds.password)}`);
14870
14867
  if (creds.database) logger$2.log(` database: ${creds.database}`);
14871
- if (creds.vhost) logger$2.log(` vhost: ${creds.vhost}`);
14872
14868
  if (creds.bucket) logger$2.log(` bucket: ${creds.bucket}`);
14873
14869
  }
14874
14870
  logger$2.log("\nConnection URLs:");
14875
14871
  if (secrets.urls.DATABASE_URL) logger$2.log(` DATABASE_URL: ${reveal ? secrets.urls.DATABASE_URL : maskUrl(secrets.urls.DATABASE_URL)}`);
14876
14872
  if (secrets.urls.REDIS_URL) logger$2.log(` REDIS_URL: ${reveal ? secrets.urls.REDIS_URL : maskUrl(secrets.urls.REDIS_URL)}`);
14877
- if (secrets.urls.RABBITMQ_URL) logger$2.log(` RABBITMQ_URL: ${reveal ? secrets.urls.RABBITMQ_URL : maskUrl(secrets.urls.RABBITMQ_URL)}`);
14878
14873
  if (secrets.urls.STORAGE_ENDPOINT) logger$2.log(` STORAGE_ENDPOINT: ${secrets.urls.STORAGE_ENDPOINT}`);
14879
14874
  if (Object.keys(secrets.custom).length > 0) {
14880
14875
  logger$2.log("\nCustom Secrets:");
@@ -15044,7 +15039,7 @@ var NoStoredSecrets = class extends Error {
15044
15039
  }
15045
15040
  };
15046
15041
  async function reconcile(secrets, workspace, stage) {
15047
- const { derivedContainers } = await import("./workspace-cBobQVoA.mjs").then((n) => n.a);
15042
+ const { derivedContainers } = await import("./workspace-DnHW8ooI.mjs").then((n) => n.a);
15048
15043
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
15049
15044
  return result ? {
15050
15045
  secrets: result.secrets,
@@ -15204,8 +15199,7 @@ function credentialedServices(containers) {
15204
15199
  redis: "redis",
15205
15200
  minio: "minio",
15206
15201
  mailpit: "mailpit",
15207
- localstack: "localstack",
15208
- rabbitmq: "rabbitmq"
15202
+ localstack: "localstack"
15209
15203
  };
15210
15204
  return [...new Set(containers.map((container) => byContainer[container]).filter((name) => Boolean(name)))];
15211
15205
  }
@@ -15225,7 +15219,7 @@ function reconcileSecrets(secrets, workspace, containers = []) {
15225
15219
  [name]: creds
15226
15220
  }
15227
15221
  };
15228
- result.urls = generateConnectionUrls(result.services, result.eventsBackend);
15222
+ result.urls = generateConnectionUrls(result.services);
15229
15223
  logger$1.log(` 🔄 Adding missing service credentials: ${name}`);
15230
15224
  changed = true;
15231
15225
  }
@@ -15243,7 +15237,7 @@ function reconcileSecrets(secrets, workspace, containers = []) {
15243
15237
  }
15244
15238
  }
15245
15239
  };
15246
- result.urls = generateConnectionUrls(result.services, result.eventsBackend);
15240
+ result.urls = generateConnectionUrls(result.services);
15247
15241
  logger$1.log(" 🔄 Adding missing service credentials: pgboss");
15248
15242
  changed = true;
15249
15243
  }
@@ -16167,7 +16161,7 @@ program.command("secrets:show").description("Show secrets for a stage").required
16167
16161
  process.exit(1);
16168
16162
  }
16169
16163
  });
16170
- 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) => {
16164
+ 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) => {
16171
16165
  try {
16172
16166
  const globalOptions = program.opts();
16173
16167
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -16222,8 +16216,8 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16222
16216
  const globalOptions = program.opts();
16223
16217
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16224
16218
  const { loadWorkspaceConfig } = await import("./config.mjs");
16225
- const { reconcileMissingSecrets } = await import("./reconcile-DhbCvh4K.mjs").then((n) => n.n);
16226
- const { readStageSecrets, writeStageSecrets } = await import("./storage-TP0x118M.mjs").then((n) => n.a);
16219
+ const { reconcileMissingSecrets } = await import("./reconcile-yNq6Shau.mjs").then((n) => n.n);
16220
+ const { readStageSecrets, writeStageSecrets } = await import("./storage-BjTLjDDp.mjs").then((n) => n.a);
16227
16221
  const { workspace } = await loadWorkspaceConfig();
16228
16222
  const stage = options.stage ?? workspace.stages.local;
16229
16223
  const secrets = await readStageSecrets(stage, workspace.root);
@@ -16231,7 +16225,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16231
16225
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
16232
16226
  process.exit(1);
16233
16227
  }
16234
- const { derivedContainers } = await import("./workspace-cBobQVoA.mjs").then((n) => n.a);
16228
+ const { derivedContainers } = await import("./workspace-DnHW8ooI.mjs").then((n) => n.a);
16235
16229
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
16236
16230
  if (!result) {
16237
16231
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
@@ -16290,7 +16284,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
16290
16284
  try {
16291
16285
  const globalOptions = program.opts();
16292
16286
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16293
- const { deployGithubCommand } = await import("./github-zkzu-ka2.mjs");
16287
+ const { deployGithubCommand } = await import("./github-DPpNfzu4.mjs");
16294
16288
  await deployGithubCommand(options);
16295
16289
  } catch (error) {
16296
16290
  console.error(formatError(error));