appos 0.3.2-0 → 0.3.4-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.
Files changed (158) hide show
  1. package/dist/bin/auth-schema-CcqAJY9P.mjs +2 -0
  2. package/dist/bin/better-sqlite3-CuQ3hsWl.mjs +2 -0
  3. package/dist/bin/bun-sql-DGeo-s_M.mjs +2 -0
  4. package/dist/bin/cache-3oO07miM.mjs +2 -0
  5. package/dist/bin/chunk-l9p7A9gZ.mjs +2 -0
  6. package/dist/bin/cockroach-BaICwY7N.mjs +2 -0
  7. package/dist/bin/database-CaysWPpa.mjs +2 -0
  8. package/dist/bin/esm-BvsccvmM.mjs +2 -0
  9. package/dist/bin/esm-CGKzJ7Am.mjs +3 -0
  10. package/dist/bin/event-DnSe3eh0.mjs +8 -0
  11. package/dist/bin/extract-blob-metadata-iqwTl2ft.mjs +170 -0
  12. package/dist/bin/generate-image-variant-Lyx0vhM6.mjs +2 -0
  13. package/dist/bin/generate-preview-0MrKxslA.mjs +2 -0
  14. package/dist/bin/libsql-DQJrZsU9.mjs +2 -0
  15. package/dist/bin/logger-BAGZLUzj.mjs +2 -0
  16. package/dist/bin/main.mjs +1201 -190
  17. package/dist/bin/migrator-B7iNKM8N.mjs +2 -0
  18. package/dist/bin/migrator-BKE1cSQQ.mjs +2 -0
  19. package/dist/bin/migrator-BXcbc9zs.mjs +2 -0
  20. package/dist/bin/migrator-B_XhRWZC.mjs +8 -0
  21. package/dist/bin/migrator-Bz52Gtr8.mjs +2 -0
  22. package/dist/bin/migrator-C7W-cZHB.mjs +2 -0
  23. package/dist/bin/migrator-CEnKyGSW.mjs +2 -0
  24. package/dist/bin/migrator-CHzIIl5X.mjs +2 -0
  25. package/dist/bin/migrator-CR-rjZdM.mjs +2 -0
  26. package/dist/bin/migrator-CjIr1ZCx.mjs +8 -0
  27. package/dist/bin/migrator-Cuubh2dg.mjs +2 -0
  28. package/dist/bin/migrator-D8m-ORbr.mjs +8 -0
  29. package/dist/bin/migrator-DBFwrhZH.mjs +2 -0
  30. package/dist/bin/migrator-DLmhW9u_.mjs +2 -0
  31. package/dist/bin/migrator-DLoHx807.mjs +4 -0
  32. package/dist/bin/migrator-DtN_iS87.mjs +2 -0
  33. package/dist/bin/migrator-Yc57lb3w.mjs +2 -0
  34. package/dist/bin/migrator-cEVXH3xC.mjs +2 -0
  35. package/dist/bin/migrator-hWi-sYIq.mjs +2 -0
  36. package/dist/bin/mysql2-DufFWkj4.mjs +2 -0
  37. package/dist/bin/neon-serverless-5a4h2VFz.mjs +2 -0
  38. package/dist/bin/node-CiOp4xrR.mjs +22 -0
  39. package/dist/bin/node-mssql-DvZGaUkB.mjs +322 -0
  40. package/dist/bin/node-postgres-BqbJVBQY.mjs +2 -0
  41. package/dist/bin/node-postgres-DnhRTTO8.mjs +2 -0
  42. package/dist/bin/open-0ksnL0S8.mjs +2 -0
  43. package/dist/bin/pdf-sUYeFPr4.mjs +14 -0
  44. package/dist/bin/pg-CaH8ptj-.mjs +2 -0
  45. package/dist/bin/pg-core-BLTZt9AH.mjs +8 -0
  46. package/dist/bin/pg-core-CGzidKaA.mjs +2 -0
  47. package/dist/bin/pglite-BJB9z7Ju.mjs +2 -0
  48. package/dist/bin/planetscale-serverless-H3RfLlMK.mjs +13 -0
  49. package/dist/bin/postgres-js-DuOf1eWm.mjs +2 -0
  50. package/dist/bin/purge-attachment-DQXpTtTx.mjs +2 -0
  51. package/dist/bin/purge-audit-logs-BEt2J2gD.mjs +2 -0
  52. package/dist/bin/{purge-unattached-blobs-Duvv8Izd.mjs → purge-unattached-blobs-DOmk4ddJ.mjs} +1 -1
  53. package/dist/bin/query-builder-DSRrR6X_.mjs +8 -0
  54. package/dist/bin/query-builder-V8-LDhvA.mjs +3 -0
  55. package/dist/bin/session-CdB1A-LB.mjs +14 -0
  56. package/dist/bin/session-Cl2e-_i8.mjs +8 -0
  57. package/dist/bin/singlestore-COft6TlR.mjs +8 -0
  58. package/dist/bin/sql-D-eKV1Dn.mjs +2 -0
  59. package/dist/bin/sqlite-cloud-Co9jOn5G.mjs +2 -0
  60. package/dist/bin/sqlite-proxy-Cpu78gJF.mjs +2 -0
  61. package/dist/bin/src-C-oXmCzx.mjs +6 -0
  62. package/dist/bin/table-3zUpWkMg.mjs +2 -0
  63. package/dist/bin/track-db-changes-DWyY5jXm.mjs +2 -0
  64. package/dist/bin/utils-CyoeCJlf.mjs +2 -0
  65. package/dist/bin/utils-EoqYQKy1.mjs +2 -0
  66. package/dist/bin/utils-bsypyqPl.mjs +2 -0
  67. package/dist/bin/vercel-postgres-HWL6xtqi.mjs +2 -0
  68. package/dist/bin/workflow-zxHDyfLq.mjs +2 -0
  69. package/dist/bin/youch-handler-DrYdbUhe.mjs +2 -0
  70. package/dist/bin/zod-MJjkEkRY.mjs +24 -0
  71. package/dist/exports/api/_virtual/rolldown_runtime.mjs +36 -1
  72. package/dist/exports/api/app-context.mjs +24 -1
  73. package/dist/exports/api/auth-schema.mjs +373 -1
  74. package/dist/exports/api/auth.d.mts +4 -0
  75. package/dist/exports/api/auth.mjs +188 -1
  76. package/dist/exports/api/cache.d.mts +2 -2
  77. package/dist/exports/api/cache.mjs +28 -1
  78. package/dist/exports/api/config.mjs +72 -1
  79. package/dist/exports/api/constants.mjs +92 -1
  80. package/dist/exports/api/container.mjs +49 -1
  81. package/dist/exports/api/database.mjs +218 -1
  82. package/dist/exports/api/event.mjs +236 -1
  83. package/dist/exports/api/i18n.mjs +45 -1
  84. package/dist/exports/api/index.mjs +20 -1
  85. package/dist/exports/api/instrumentation.mjs +40 -1
  86. package/dist/exports/api/logger.mjs +26 -1
  87. package/dist/exports/api/mailer.mjs +37 -1
  88. package/dist/exports/api/middleware.mjs +73 -1
  89. package/dist/exports/api/openapi.mjs +507 -1
  90. package/dist/exports/api/orm.mjs +43 -1
  91. package/dist/exports/api/otel.mjs +56 -1
  92. package/dist/exports/api/redis.mjs +41 -1
  93. package/dist/exports/api/storage-schema.mjs +72 -1
  94. package/dist/exports/api/storage.mjs +833 -1
  95. package/dist/exports/api/web/auth.mjs +17 -1
  96. package/dist/exports/api/workflow.mjs +196 -1
  97. package/dist/exports/api/workflows/_virtual/rolldown_runtime.mjs +36 -1
  98. package/dist/exports/api/workflows/api/auth-schema.mjs +373 -1
  99. package/dist/exports/api/workflows/api/auth.d.mts +4 -0
  100. package/dist/exports/api/workflows/api/cache.d.mts +2 -2
  101. package/dist/exports/api/workflows/api/event.mjs +126 -1
  102. package/dist/exports/api/workflows/api/redis.mjs +3 -1
  103. package/dist/exports/api/workflows/api/workflow.mjs +135 -1
  104. package/dist/exports/api/workflows/constants.mjs +23 -1
  105. package/dist/exports/api/workflows/extract-blob-metadata.mjs +132 -1
  106. package/dist/exports/api/workflows/generate-image-variant.d.mts +2 -2
  107. package/dist/exports/api/workflows/generate-image-variant.mjs +118 -1
  108. package/dist/exports/api/workflows/generate-preview.mjs +160 -1
  109. package/dist/exports/api/workflows/index.mjs +3 -1
  110. package/dist/exports/api/workflows/purge-attachment.mjs +34 -1
  111. package/dist/exports/api/workflows/purge-audit-logs.mjs +47 -1
  112. package/dist/exports/api/workflows/purge-unattached-blobs.mjs +46 -1
  113. package/dist/exports/api/workflows/track-db-changes.mjs +110 -1
  114. package/dist/exports/cli/_virtual/rolldown_runtime.mjs +36 -1
  115. package/dist/exports/cli/api/auth-schema.mjs +373 -1
  116. package/dist/exports/cli/api/auth.d.mts +4 -0
  117. package/dist/exports/cli/api/cache.d.mts +2 -2
  118. package/dist/exports/cli/api/event.mjs +126 -1
  119. package/dist/exports/cli/api/redis.mjs +3 -1
  120. package/dist/exports/cli/api/workflow.mjs +135 -1
  121. package/dist/exports/cli/api/workflows/extract-blob-metadata.mjs +132 -1
  122. package/dist/exports/cli/api/workflows/generate-image-variant.mjs +118 -1
  123. package/dist/exports/cli/api/workflows/generate-preview.mjs +160 -1
  124. package/dist/exports/cli/api/workflows/purge-attachment.mjs +34 -1
  125. package/dist/exports/cli/api/workflows/purge-audit-logs.mjs +47 -1
  126. package/dist/exports/cli/api/workflows/purge-unattached-blobs.mjs +46 -1
  127. package/dist/exports/cli/api/workflows/track-db-changes.mjs +110 -1
  128. package/dist/exports/cli/command.d.mts +2 -0
  129. package/dist/exports/cli/command.mjs +43 -1
  130. package/dist/exports/cli/constants.mjs +23 -1
  131. package/dist/exports/cli/index.mjs +3 -1
  132. package/dist/exports/devtools/index.js +4 -1
  133. package/dist/exports/tests/api/auth.d.mts +4 -0
  134. package/dist/exports/tests/api/cache.d.mts +2 -2
  135. package/dist/exports/tests/api/middleware/i18n.mjs +1 -1
  136. package/dist/exports/tests/api/middleware/youch-handler.mjs +1 -1
  137. package/dist/exports/tests/api/openapi.mjs +1 -1
  138. package/dist/exports/tests/api/server.mjs +1 -1
  139. package/dist/exports/tests/api/storage.d.mts +4 -4
  140. package/dist/exports/tests/constants.mjs +1 -1
  141. package/dist/exports/vendors/date.js +1 -1
  142. package/dist/exports/vendors/toolkit.js +1 -1
  143. package/dist/exports/vendors/zod.js +1 -1
  144. package/dist/exports/vitest/globals.mjs +1 -1
  145. package/dist/exports/web/auth.js +75 -1
  146. package/dist/exports/web/i18n.js +45 -1
  147. package/dist/exports/web/index.js +8 -1
  148. package/package.json +19 -18
  149. package/dist/bin/auth-schema-Va0CYicu.mjs +0 -2
  150. package/dist/bin/event-8JibGFH_.mjs +0 -2
  151. package/dist/bin/extract-blob-metadata-DjPfHtQ2.mjs +0 -2
  152. package/dist/bin/generate-image-variant-D5VDFyWj.mjs +0 -2
  153. package/dist/bin/generate-preview-Dssw7w5U.mjs +0 -2
  154. package/dist/bin/purge-attachment-BBPzIxwt.mjs +0 -2
  155. package/dist/bin/purge-audit-logs-BeZy3IFM.mjs +0 -2
  156. package/dist/bin/track-db-changes-CFykw_YO.mjs +0 -2
  157. package/dist/bin/workflow-BNUZrj4F.mjs +0 -2
  158. package/dist/bin/youch-handler-BadUgHb0.mjs +0 -2
@@ -1 +1,28 @@
1
- import{defineRedisClient as e}from"./redis.mjs";import{createKeyv as t}from"@keyv/redis";function n({url:n,logger:r,options:i}){let a=t(e({logger:r,url:n}),i);return a.on(`error`,e=>{r.error({err:e},`Cache Keyv error`)}),a}export{n as defineCache};
1
+ import { defineRedisClient } from "./redis.mjs";
2
+ import { createKeyv } from "@keyv/redis";
3
+
4
+ //#region src/api/cache.ts
5
+ /**
6
+ * Define the cache instance using shared Redis client.
7
+ * Connection is lazy - only connects when first cache operation is performed.
8
+ *
9
+ * Algorithm:
10
+ * 1. Create Redis client using defineRedisClient() (lazy connection)
11
+ * 2. Pass client to createKeyv() - connection happens on first use
12
+ *
13
+ * @param opts - The options for defining the cache.
14
+ * @returns The cache instance.
15
+ */
16
+ function defineCache({ url, logger, options }) {
17
+ const cache = createKeyv(defineRedisClient({
18
+ logger,
19
+ url
20
+ }), options);
21
+ cache.on("error", (err) => {
22
+ logger.error({ err }, "Cache Keyv error");
23
+ });
24
+ return cache;
25
+ }
26
+
27
+ //#endregion
28
+ export { defineCache };
@@ -1 +1,72 @@
1
- import{z as e}from"zod";const t=e.object({APP_NAME:e.string().default(`AppOS`),APP_DESC:e.string().default(`The app operating system to build your business.`),APP_VERSION:e.string().default(`development`)});function n(e){let t={...e},n=new Set;function r(e,i){if(n.has(e))throw Error(`Circular reference detected in environment variable: ${e}`);n.add(e);let a=i.replace(/\{\{([^}]+)\}\}/g,(e,n)=>{let i=n.trim();if(!i)return e;let a=t[i];return a===void 0?e:a.includes(`{{`)?r(i,a):a});return n.delete(e),a}for(let[e,n]of Object.entries(t))n?.includes(`{{`)&&(t[e]=r(e,n));return t}function r(e){let r=t.extend(e.shape),i={};for(let[e,t]of Object.entries(r.shape)){let n=t;for(;n;){let t=n.def;if(t.defaultValue!==void 0){let n=typeof t.defaultValue==`function`?t.defaultValue():t.defaultValue;typeof n==`string`&&(i[e]=n);break}n=t.innerType||t.schema}}let a={...i};for(let[e,t]of Object.entries(process.env))t!==void 0&&(a[e]=t);return r.parse(n(a))}export{t as baseSchema,r as defineConfig};
1
+ import { z } from "zod";
2
+
3
+ //#region src/api/config.ts
4
+ /**
5
+ * The config base schema.
6
+ */
7
+ const baseSchema = z.object({
8
+ APP_NAME: z.string().default("AppOS"),
9
+ APP_DESC: z.string().default("The app operating system to build your business."),
10
+ APP_VERSION: z.string().default("development")
11
+ });
12
+ /**
13
+ * Expands variables in environment values using {{VAR_NAME}} syntax.
14
+ * Supports nested expansion and handles circular references.
15
+ *
16
+ * If a variable is not found, the placeholder is preserved in the output.
17
+ * Variables can reference other variables recursively.
18
+ *
19
+ * @param env Environment variables object
20
+ * @returns Environment variables with expanded values
21
+ */
22
+ function expandVariables(env) {
23
+ const expanded = { ...env };
24
+ const expanding = /* @__PURE__ */ new Set();
25
+ function expand(key, value) {
26
+ if (expanding.has(key)) throw new Error(`Circular reference detected in environment variable: ${key}`);
27
+ expanding.add(key);
28
+ const result = value.replace(/\{\{([^}]+)\}\}/g, (match, varName) => {
29
+ const trimmedVarName = varName.trim();
30
+ if (!trimmedVarName) return match;
31
+ const varValue = expanded[trimmedVarName];
32
+ if (varValue === void 0) return match;
33
+ if (varValue.includes("{{")) return expand(trimmedVarName, varValue);
34
+ return varValue;
35
+ });
36
+ expanding.delete(key);
37
+ return result;
38
+ }
39
+ for (const [key, value] of Object.entries(expanded)) if (value?.includes("{{")) expanded[key] = expand(key, value);
40
+ return expanded;
41
+ }
42
+ /**
43
+ * Creates a configuration object by merging base config with user-defined schema.
44
+ *
45
+ * Variables in default values are expanded after defaults are applied.
46
+ * For example: `DATABASE_URL: z.string().default("postgres://{{DB_HOST}}:5432")`
47
+ *
48
+ * @param userSchema User-defined Zod schema to merge with base config.
49
+ * @returns Parsed and validated configuration object.
50
+ */
51
+ function defineConfig(userSchema) {
52
+ const mergedSchema = baseSchema.extend(userSchema.shape);
53
+ const defaults = {};
54
+ for (const [key, fieldSchema] of Object.entries(mergedSchema.shape)) {
55
+ let current = fieldSchema;
56
+ while (current) {
57
+ const def = current.def;
58
+ if (def.defaultValue !== void 0) {
59
+ const defaultValue = typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
60
+ if (typeof defaultValue === "string") defaults[key] = defaultValue;
61
+ break;
62
+ }
63
+ current = def.innerType || def.schema;
64
+ }
65
+ }
66
+ const merged = { ...defaults };
67
+ for (const [key, value] of Object.entries(process.env)) if (value !== void 0) merged[key] = value;
68
+ return mergedSchema.parse(expandVariables(merged));
69
+ }
70
+
71
+ //#endregion
72
+ export { baseSchema, defineConfig };
@@ -1 +1,92 @@
1
- const e=`api`,t=`databases`,n=`routes`,r=`workflows`,i=`events`,a=`public`,o=`locales`,s=process.env.NODE_ENV===`production`?`js`:`ts`;export{e as APPOS_DIR,t as DATABASES_DIR,i as EVENTS_DIR,s as FILE_EXT,o as LOCALES_DIR,a as PUBLIC_DIR,n as ROUTES_DIR,r as WORKFLOWS_DIR};
1
+ //#region src/constants.ts
2
+ /**
3
+ * Directory constants used throughout the AppOS framework.
4
+ *
5
+ * These constants define the conventional directory structure for AppOS applications.
6
+ * All paths are relative to the project root unless otherwise specified.
7
+ */
8
+ /**
9
+ * The main AppOS directory containing application code.
10
+ *
11
+ * Expected structure:
12
+ * - `<project-root>/api/container.ts` - Application container definition
13
+ * - `<project-root>/api/commands/` - CLI commands
14
+ * - `<project-root>/api/databases/` - Database schemas and migrations
15
+ * - `<project-root>/api/emails/` - Email templates
16
+ * - `<project-root>/api/events/` - Event definitions
17
+ * - `<project-root>/api/middleware/` - Custom middleware
18
+ * - `<project-root>/api/routes/` - API routes
19
+ * - `<project-root>/api/workflows/` - Background workflows
20
+ *
21
+ * @default "api"
22
+ */
23
+ const APPOS_DIR = "api";
24
+ /**
25
+ * Directory for database schemas and migrations.
26
+ *
27
+ * Expected structure:
28
+ * - `<APPOS_DIR>/databases/<db-name>/schema.ts` - Database schema
29
+ * - `<APPOS_DIR>/databases/<db-name>/schema-migrations/` - Migration files
30
+ *
31
+ * @default "databases"
32
+ */
33
+ const DATABASES_DIR = "databases";
34
+ /**
35
+ * Directory for API route handlers.
36
+ *
37
+ * Expected structure:
38
+ * - `<APPOS_DIR>/routes/<route-name>.ts` - Route handler files
39
+ *
40
+ * @default "routes"
41
+ */
42
+ const ROUTES_DIR = "routes";
43
+ /**
44
+ * Directory for background workflow definitions.
45
+ *
46
+ * Expected structure:
47
+ * - `<APPOS_DIR>/workflows/<workflow-name>.ts` - Workflow files
48
+ *
49
+ * @default "workflows"
50
+ */
51
+ const WORKFLOWS_DIR = "workflows";
52
+ /**
53
+ * Directory for event definitions and subscriptions.
54
+ *
55
+ * Expected structure:
56
+ * - `<APPOS_DIR>/events/<event-name>.ts` - Event definition files
57
+ * - `<APPOS_DIR>/events/<subscription-name>.ts` - Event subscription files
58
+ *
59
+ * @default "events"
60
+ */
61
+ const EVENTS_DIR = "events";
62
+ /**
63
+ * Directory for public static assets.
64
+ *
65
+ * Expected structure:
66
+ * - `<project-root>/public/` - Public directory root
67
+ * - `<project-root>/public/locales/` - i18n translation files
68
+ *
69
+ * @default "public"
70
+ */
71
+ const PUBLIC_DIR = process.env.NODE_ENV === "production" ? "client" : "public";
72
+ /**
73
+ * Directory for i18n locale/translation files (relative to PUBLIC_DIR).
74
+ *
75
+ * Expected structure:
76
+ * - `<PUBLIC_DIR>/locales/<lng>/<ns>.json` - Translation files
77
+ *
78
+ * @default "locales"
79
+ */
80
+ const LOCALES_DIR = "locales";
81
+ /**
82
+ * File extension for code files based on environment.
83
+ *
84
+ * In development: `.ts` (TypeScript source files)
85
+ * In production: `.js` (bundled JavaScript files)
86
+ *
87
+ * @default "ts" in development, "js" in production
88
+ */
89
+ const FILE_EXT = process.env.NODE_ENV === "production" ? "js" : "ts";
90
+
91
+ //#endregion
92
+ export { APPOS_DIR, DATABASES_DIR, EVENTS_DIR, FILE_EXT, LOCALES_DIR, PUBLIC_DIR, ROUTES_DIR, WORKFLOWS_DIR };
@@ -1 +1,49 @@
1
- function e(e){return e}export{e as defineAppContainer};
1
+ //#region src/api/container.ts
2
+ /**
3
+ * Defines an application container with type-safe validation and inference.
4
+ *
5
+ * This helper function ensures that:
6
+ * 1. All required base fields (config, cache, db) are provided
7
+ * 2. Custom fields are fully preserved in the inferred type
8
+ * 3. TypeScript provides autocomplete for all fields in commands/routes
9
+ *
10
+ * @template T - The container type extending AppContainer
11
+ * @param container - The container object with required and custom fields
12
+ * @returns The same container object with full type inference
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * // In your api/main.ts file:
17
+ * export async function defineContainer() {
18
+ * const config = defineConfig(z.object({
19
+ * PORT: z.coerce.number().default(3000),
20
+ * EVENTS_DB_URL: z.string(),
21
+ * WORKER_DB_URL: z.string(),
22
+ * }));
23
+ * const logger = defineLogger({ level: config.LOG_LEVEL });
24
+ *
25
+ * return defineAppContainer({
26
+ * auth: defineAuth({ config }),
27
+ * cache: {},
28
+ * config,
29
+ * db: {},
30
+ * eventBus: defineEventBus({ dbUrl: config.EVENTS_DB_URL, logger }),
31
+ * i18n: await defineI18n({ ... }),
32
+ * logger,
33
+ * mailer: defineMailer({ ... }),
34
+ * server: {
35
+ * port: config.PORT,
36
+ * },
37
+ * worker: {
38
+ * dbUrl: config.WORKER_DB_URL,
39
+ * },
40
+ * });
41
+ * }
42
+ * ```
43
+ */
44
+ function defineAppContainer(container) {
45
+ return container;
46
+ }
47
+
48
+ //#endregion
49
+ export { defineAppContainer };
@@ -1 +1,218 @@
1
- import{APPOS_DIR as e,DATABASES_DIR as t}from"./constants.mjs";import{getTableName as n,sql as r}from"drizzle-orm";import{join as i}from"node:path";import{drizzle as a}from"drizzle-orm/node-postgres";import{migrate as o}from"drizzle-orm/node-postgres/migrator";import{Client as s,Pool as c}from"pg";const l=`public`;function u(n,r=`schema`){let a=r===`schema`?`schema-migrations`:`data-migrations`,o=r===`schema`?`schema-migrations`:`data-migrations`;return{migrationsFolder:`${i(e,t)}/${n}/${a}`,migrationsSchema:l,migrationsTable:o}}var d=class{#e;constructor(e){this.#e=e}logQuery(e,t){this.#e.info(t,e)}};function f(e){let t=n(e);return{_table:r`${r.raw(`'${t}'`)}`.as(`_table`),old:r`to_jsonb(OLD.*)`.as(`old`),new:r`to_jsonb(NEW.*)`.as(`new`)}}async function p(e){return a({client:new c({application_name:`appos`,max:16,...e.poolConfig}),logger:new d(e.logger),relations:e.relations,schema:e.schema})}async function m(e){let t=`test_w${process.env.VITEST_POOL_ID||`0`}_${Date.now()}_${Math.random().toString(36).substring(2,8)}`;await y(e.connectionString,e.name,e.logger);let n=`${e.name}_test_template`,r=_(e.connectionString),i=new s({connectionString:r});await i.connect();try{await i.query(`CREATE DATABASE ${t} WITH TEMPLATE ${n}`)}finally{await i.end()}let o=h(e.connectionString);o.database=t;let l=new c({connectionString:g(o)});return{cleanUp:async()=>{await l.end();let e=new s({connectionString:r});await e.connect();try{await e.query(`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1`,[t]),await e.query(`DROP DATABASE IF EXISTS ${t}`)}catch(e){console.error(`Could not drop test database:`,e)}finally{await e.end()}},db:a({client:l,logger:e.logger?new d(e.logger):void 0,relations:e.relations,schema:e.schema}),dbName:t}}function h(e){let t=new URL(e);return{database:t.pathname.slice(1)||`postgres`,host:t.hostname,password:decodeURIComponent(t.password),port:Number.parseInt(t.port||`5432`,10),user:decodeURIComponent(t.username)}}function g(e){let{user:t,password:n,host:r,port:i,database:a}=e;return`postgresql://${encodeURIComponent(t)}:${encodeURIComponent(n)}@${r}:${i}/${a}`}function _(e){let t=h(e);return t.database=`postgres`,g(t)}function v(e){let t=0;for(let n=0;n<e.length;n++){let r=e.charCodeAt(n);t=(t<<5)-t+r,t&=t}return Math.abs(t)}async function y(e,t,n){let r=`${t}_test_template`,i=new s({connectionString:_(e)});await i.connect();let l=v(r);try{if(await i.query(`SELECT pg_advisory_lock($1)`,[l]),(await i.query(`SELECT 1 FROM pg_database WHERE datname = $1`,[r])).rowCount===0){await i.query(`CREATE DATABASE ${r} WITH IS_TEMPLATE = true`);let s=h(e);s.database=r;let l=new c({connectionString:g(s)});try{await o(a({client:l,logger:n?new d(n):void 0}),u(t))}finally{await l.end()}}}finally{await i.query(`SELECT pg_advisory_unlock($1)`,[l]),await i.end()}}export{f as dbChanges,p as defineDatabase,u as defineMigrationOpts,m as defineTestDatabase,l as migrationsSchema};
1
+ import { APPOS_DIR, DATABASES_DIR } from "./constants.mjs";
2
+ import { getTableName, sql } from "drizzle-orm";
3
+ import { join } from "node:path";
4
+ import { drizzle } from "drizzle-orm/node-postgres";
5
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
6
+ import { Client, Pool } from "pg";
7
+
8
+ //#region src/api/database.ts
9
+ /**
10
+ * The schema used for migrations.
11
+ */
12
+ const migrationsSchema = "public";
13
+ /**
14
+ * Get the migration options for a specific database.
15
+ *
16
+ * @param name Name of the database to get migration options for
17
+ * @param type Type of migration (schema or data), defaults to "schema"
18
+ * @returns Migration configuration for drizzle-orm
19
+ */
20
+ function defineMigrationOpts(name, type = "schema") {
21
+ const folder = type === "schema" ? "schema-migrations" : "data-migrations";
22
+ const table = type === "schema" ? "schema-migrations" : "data-migrations";
23
+ return {
24
+ migrationsFolder: `${join(APPOS_DIR, DATABASES_DIR)}/${name}/${folder}`,
25
+ migrationsSchema,
26
+ migrationsTable: table
27
+ };
28
+ }
29
+ /**
30
+ * The database logger.
31
+ */
32
+ var DatabaseLogger = class {
33
+ #logger;
34
+ constructor(logger) {
35
+ this.#logger = logger;
36
+ }
37
+ logQuery(query, params) {
38
+ this.#logger.info(params, query);
39
+ }
40
+ };
41
+ /**
42
+ * Generate old and new row JSONB representations for delete/insert/update queries.
43
+ * Uses PostgreSQL 18's OLD/NEW support in RETURNING clause.
44
+ *
45
+ * @param table The table to generate changes for.
46
+ * @returns Object containing `_table`, `old` and `new` JSONB columns.
47
+ */
48
+ function dbChanges(table) {
49
+ const tableName = getTableName(table);
50
+ return {
51
+ _table: sql`${sql.raw(`'${tableName}'`)}`.as("_table"),
52
+ old: sql`to_jsonb(OLD.*)`.as("old"),
53
+ new: sql`to_jsonb(NEW.*)`.as("new")
54
+ };
55
+ }
56
+ /**
57
+ * Define the database with the provided options.
58
+ *
59
+ * Algorithm:
60
+ * 1. Create a connection pool with sensible defaults
61
+ * 2. Initialize drizzle ORM with schema and relations
62
+ *
63
+ * @param opts The options for defining the database, including pool configuration, relations, and schema.
64
+ * @template TSchema The schema type for the database.
65
+ * @template TRelations The relations type for the database.
66
+ * @returns The defined database instance.
67
+ */
68
+ async function defineDatabase(opts) {
69
+ return drizzle({
70
+ client: new Pool({
71
+ application_name: "appos",
72
+ max: 16,
73
+ ...opts.poolConfig
74
+ }),
75
+ logger: new DatabaseLogger(opts.logger),
76
+ relations: opts.relations,
77
+ schema: opts.schema
78
+ });
79
+ }
80
+ /**
81
+ * Create a single test database with isolated schema.
82
+ *
83
+ * @param connectionString - Database connection string
84
+ * @param name - Name of the test database
85
+ * @returns Test database instance and cleanup function
86
+ */
87
+ async function defineTestDatabase(opts) {
88
+ const dbName = `test_w${process.env.VITEST_POOL_ID || "0"}_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
89
+ await ensureTemplateDatabase(opts.connectionString, opts.name, opts.logger);
90
+ const templateDbName = `${opts.name}_test_template`;
91
+ const maintenanceUrl = getMaintenanceDbUrl(opts.connectionString);
92
+ const maintenanceClient = new Client({ connectionString: maintenanceUrl });
93
+ await maintenanceClient.connect();
94
+ try {
95
+ await maintenanceClient.query(`CREATE DATABASE ${dbName} WITH TEMPLATE ${templateDbName}`);
96
+ } finally {
97
+ await maintenanceClient.end();
98
+ }
99
+ const testComponents = parsePostgresUrl(opts.connectionString);
100
+ testComponents.database = dbName;
101
+ const testPool = new Pool({ connectionString: buildPostgresUrl(testComponents) });
102
+ const db = drizzle({
103
+ client: testPool,
104
+ logger: opts.logger ? new DatabaseLogger(opts.logger) : void 0,
105
+ relations: opts.relations,
106
+ schema: opts.schema
107
+ });
108
+ const cleanUp = async () => {
109
+ await testPool.end();
110
+ const cleanupClient = new Client({ connectionString: maintenanceUrl });
111
+ await cleanupClient.connect();
112
+ try {
113
+ await cleanupClient.query(`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1`, [dbName]);
114
+ await cleanupClient.query(`DROP DATABASE IF EXISTS ${dbName}`);
115
+ } catch (error) {
116
+ console.error("Could not drop test database:", error);
117
+ } finally {
118
+ await cleanupClient.end();
119
+ }
120
+ };
121
+ return {
122
+ cleanUp,
123
+ db,
124
+ dbName
125
+ };
126
+ }
127
+ /**
128
+ * Parse PostgreSQL connection URL into components.
129
+ *
130
+ * @param connectionString - PostgreSQL connection URL
131
+ * @returns Parsed URL components
132
+ */
133
+ function parsePostgresUrl(connectionString) {
134
+ const url = new URL(connectionString);
135
+ return {
136
+ database: url.pathname.slice(1) || "postgres",
137
+ host: url.hostname,
138
+ password: decodeURIComponent(url.password),
139
+ port: Number.parseInt(url.port || "5432", 10),
140
+ user: decodeURIComponent(url.username)
141
+ };
142
+ }
143
+ /**
144
+ * Build PostgreSQL connection URL from components.
145
+ *
146
+ * @param components - URL components
147
+ * @returns PostgreSQL connection URL
148
+ */
149
+ function buildPostgresUrl(components) {
150
+ const { user, password, host, port, database } = components;
151
+ return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${database}`;
152
+ }
153
+ /**
154
+ * Get connection URL for maintenance database operations.
155
+ *
156
+ * @param connectionString - Original connection string
157
+ * @returns Connection string to maintenance database
158
+ */
159
+ function getMaintenanceDbUrl(connectionString) {
160
+ const components = parsePostgresUrl(connectionString);
161
+ components.database = "postgres";
162
+ return buildPostgresUrl(components);
163
+ }
164
+ /**
165
+ * Generate deterministic advisory lock ID from template database name.
166
+ *
167
+ * @param templateDbName - Template database name
168
+ * @returns 32-bit integer lock ID
169
+ */
170
+ function getAdvisoryLockId(templateDbName) {
171
+ let hash = 0;
172
+ for (let i = 0; i < templateDbName.length; i++) {
173
+ const char = templateDbName.charCodeAt(i);
174
+ hash = (hash << 5) - hash + char;
175
+ hash = hash & hash;
176
+ }
177
+ return Math.abs(hash);
178
+ }
179
+ /**
180
+ * Ensure template database exists with migrations applied.
181
+ *
182
+ * Uses PostgreSQL advisory locks to prevent race conditions when multiple
183
+ * test workers try to create the same template database concurrently.
184
+ *
185
+ * @param connectionString - Database connection string
186
+ * @param name - Database name (e.g., "platform")
187
+ * @param rootFolder - Root folder for migrations
188
+ * @param logger - Optional logger
189
+ */
190
+ async function ensureTemplateDatabase(connectionString, name, logger) {
191
+ const templateDbName = `${name}_test_template`;
192
+ const maintenanceClient = new Client({ connectionString: getMaintenanceDbUrl(connectionString) });
193
+ await maintenanceClient.connect();
194
+ const lockId = getAdvisoryLockId(templateDbName);
195
+ try {
196
+ await maintenanceClient.query("SELECT pg_advisory_lock($1)", [lockId]);
197
+ if ((await maintenanceClient.query("SELECT 1 FROM pg_database WHERE datname = $1", [templateDbName])).rowCount === 0) {
198
+ await maintenanceClient.query(`CREATE DATABASE ${templateDbName} WITH IS_TEMPLATE = true`);
199
+ const components = parsePostgresUrl(connectionString);
200
+ components.database = templateDbName;
201
+ const templatePool = new Pool({ connectionString: buildPostgresUrl(components) });
202
+ try {
203
+ await migrate(drizzle({
204
+ client: templatePool,
205
+ logger: logger ? new DatabaseLogger(logger) : void 0
206
+ }), defineMigrationOpts(name));
207
+ } finally {
208
+ await templatePool.end();
209
+ }
210
+ }
211
+ } finally {
212
+ await maintenanceClient.query("SELECT pg_advisory_unlock($1)", [lockId]);
213
+ await maintenanceClient.end();
214
+ }
215
+ }
216
+
217
+ //#endregion
218
+ export { dbChanges, defineDatabase, defineMigrationOpts, defineTestDatabase, migrationsSchema };