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.
- package/dist/bin/auth-schema-CcqAJY9P.mjs +2 -0
- package/dist/bin/better-sqlite3-CuQ3hsWl.mjs +2 -0
- package/dist/bin/bun-sql-DGeo-s_M.mjs +2 -0
- package/dist/bin/cache-3oO07miM.mjs +2 -0
- package/dist/bin/chunk-l9p7A9gZ.mjs +2 -0
- package/dist/bin/cockroach-BaICwY7N.mjs +2 -0
- package/dist/bin/database-CaysWPpa.mjs +2 -0
- package/dist/bin/esm-BvsccvmM.mjs +2 -0
- package/dist/bin/esm-CGKzJ7Am.mjs +3 -0
- package/dist/bin/event-DnSe3eh0.mjs +8 -0
- package/dist/bin/extract-blob-metadata-iqwTl2ft.mjs +170 -0
- package/dist/bin/generate-image-variant-Lyx0vhM6.mjs +2 -0
- package/dist/bin/generate-preview-0MrKxslA.mjs +2 -0
- package/dist/bin/libsql-DQJrZsU9.mjs +2 -0
- package/dist/bin/logger-BAGZLUzj.mjs +2 -0
- package/dist/bin/main.mjs +1201 -190
- package/dist/bin/migrator-B7iNKM8N.mjs +2 -0
- package/dist/bin/migrator-BKE1cSQQ.mjs +2 -0
- package/dist/bin/migrator-BXcbc9zs.mjs +2 -0
- package/dist/bin/migrator-B_XhRWZC.mjs +8 -0
- package/dist/bin/migrator-Bz52Gtr8.mjs +2 -0
- package/dist/bin/migrator-C7W-cZHB.mjs +2 -0
- package/dist/bin/migrator-CEnKyGSW.mjs +2 -0
- package/dist/bin/migrator-CHzIIl5X.mjs +2 -0
- package/dist/bin/migrator-CR-rjZdM.mjs +2 -0
- package/dist/bin/migrator-CjIr1ZCx.mjs +8 -0
- package/dist/bin/migrator-Cuubh2dg.mjs +2 -0
- package/dist/bin/migrator-D8m-ORbr.mjs +8 -0
- package/dist/bin/migrator-DBFwrhZH.mjs +2 -0
- package/dist/bin/migrator-DLmhW9u_.mjs +2 -0
- package/dist/bin/migrator-DLoHx807.mjs +4 -0
- package/dist/bin/migrator-DtN_iS87.mjs +2 -0
- package/dist/bin/migrator-Yc57lb3w.mjs +2 -0
- package/dist/bin/migrator-cEVXH3xC.mjs +2 -0
- package/dist/bin/migrator-hWi-sYIq.mjs +2 -0
- package/dist/bin/mysql2-DufFWkj4.mjs +2 -0
- package/dist/bin/neon-serverless-5a4h2VFz.mjs +2 -0
- package/dist/bin/node-CiOp4xrR.mjs +22 -0
- package/dist/bin/node-mssql-DvZGaUkB.mjs +322 -0
- package/dist/bin/node-postgres-BqbJVBQY.mjs +2 -0
- package/dist/bin/node-postgres-DnhRTTO8.mjs +2 -0
- package/dist/bin/open-0ksnL0S8.mjs +2 -0
- package/dist/bin/pdf-sUYeFPr4.mjs +14 -0
- package/dist/bin/pg-CaH8ptj-.mjs +2 -0
- package/dist/bin/pg-core-BLTZt9AH.mjs +8 -0
- package/dist/bin/pg-core-CGzidKaA.mjs +2 -0
- package/dist/bin/pglite-BJB9z7Ju.mjs +2 -0
- package/dist/bin/planetscale-serverless-H3RfLlMK.mjs +13 -0
- package/dist/bin/postgres-js-DuOf1eWm.mjs +2 -0
- package/dist/bin/purge-attachment-DQXpTtTx.mjs +2 -0
- package/dist/bin/purge-audit-logs-BEt2J2gD.mjs +2 -0
- package/dist/bin/{purge-unattached-blobs-Duvv8Izd.mjs → purge-unattached-blobs-DOmk4ddJ.mjs} +1 -1
- package/dist/bin/query-builder-DSRrR6X_.mjs +8 -0
- package/dist/bin/query-builder-V8-LDhvA.mjs +3 -0
- package/dist/bin/session-CdB1A-LB.mjs +14 -0
- package/dist/bin/session-Cl2e-_i8.mjs +8 -0
- package/dist/bin/singlestore-COft6TlR.mjs +8 -0
- package/dist/bin/sql-D-eKV1Dn.mjs +2 -0
- package/dist/bin/sqlite-cloud-Co9jOn5G.mjs +2 -0
- package/dist/bin/sqlite-proxy-Cpu78gJF.mjs +2 -0
- package/dist/bin/src-C-oXmCzx.mjs +6 -0
- package/dist/bin/table-3zUpWkMg.mjs +2 -0
- package/dist/bin/track-db-changes-DWyY5jXm.mjs +2 -0
- package/dist/bin/utils-CyoeCJlf.mjs +2 -0
- package/dist/bin/utils-EoqYQKy1.mjs +2 -0
- package/dist/bin/utils-bsypyqPl.mjs +2 -0
- package/dist/bin/vercel-postgres-HWL6xtqi.mjs +2 -0
- package/dist/bin/workflow-zxHDyfLq.mjs +2 -0
- package/dist/bin/youch-handler-DrYdbUhe.mjs +2 -0
- package/dist/bin/zod-MJjkEkRY.mjs +24 -0
- package/dist/exports/api/_virtual/rolldown_runtime.mjs +36 -1
- package/dist/exports/api/app-context.mjs +24 -1
- package/dist/exports/api/auth-schema.mjs +373 -1
- package/dist/exports/api/auth.d.mts +4 -0
- package/dist/exports/api/auth.mjs +188 -1
- package/dist/exports/api/cache.d.mts +2 -2
- package/dist/exports/api/cache.mjs +28 -1
- package/dist/exports/api/config.mjs +72 -1
- package/dist/exports/api/constants.mjs +92 -1
- package/dist/exports/api/container.mjs +49 -1
- package/dist/exports/api/database.mjs +218 -1
- package/dist/exports/api/event.mjs +236 -1
- package/dist/exports/api/i18n.mjs +45 -1
- package/dist/exports/api/index.mjs +20 -1
- package/dist/exports/api/instrumentation.mjs +40 -1
- package/dist/exports/api/logger.mjs +26 -1
- package/dist/exports/api/mailer.mjs +37 -1
- package/dist/exports/api/middleware.mjs +73 -1
- package/dist/exports/api/openapi.mjs +507 -1
- package/dist/exports/api/orm.mjs +43 -1
- package/dist/exports/api/otel.mjs +56 -1
- package/dist/exports/api/redis.mjs +41 -1
- package/dist/exports/api/storage-schema.mjs +72 -1
- package/dist/exports/api/storage.mjs +833 -1
- package/dist/exports/api/web/auth.mjs +17 -1
- package/dist/exports/api/workflow.mjs +196 -1
- package/dist/exports/api/workflows/_virtual/rolldown_runtime.mjs +36 -1
- package/dist/exports/api/workflows/api/auth-schema.mjs +373 -1
- package/dist/exports/api/workflows/api/auth.d.mts +4 -0
- package/dist/exports/api/workflows/api/cache.d.mts +2 -2
- package/dist/exports/api/workflows/api/event.mjs +126 -1
- package/dist/exports/api/workflows/api/redis.mjs +3 -1
- package/dist/exports/api/workflows/api/workflow.mjs +135 -1
- package/dist/exports/api/workflows/constants.mjs +23 -1
- package/dist/exports/api/workflows/extract-blob-metadata.mjs +132 -1
- package/dist/exports/api/workflows/generate-image-variant.d.mts +2 -2
- package/dist/exports/api/workflows/generate-image-variant.mjs +118 -1
- package/dist/exports/api/workflows/generate-preview.mjs +160 -1
- package/dist/exports/api/workflows/index.mjs +3 -1
- package/dist/exports/api/workflows/purge-attachment.mjs +34 -1
- package/dist/exports/api/workflows/purge-audit-logs.mjs +47 -1
- package/dist/exports/api/workflows/purge-unattached-blobs.mjs +46 -1
- package/dist/exports/api/workflows/track-db-changes.mjs +110 -1
- package/dist/exports/cli/_virtual/rolldown_runtime.mjs +36 -1
- package/dist/exports/cli/api/auth-schema.mjs +373 -1
- package/dist/exports/cli/api/auth.d.mts +4 -0
- package/dist/exports/cli/api/cache.d.mts +2 -2
- package/dist/exports/cli/api/event.mjs +126 -1
- package/dist/exports/cli/api/redis.mjs +3 -1
- package/dist/exports/cli/api/workflow.mjs +135 -1
- package/dist/exports/cli/api/workflows/extract-blob-metadata.mjs +132 -1
- package/dist/exports/cli/api/workflows/generate-image-variant.mjs +118 -1
- package/dist/exports/cli/api/workflows/generate-preview.mjs +160 -1
- package/dist/exports/cli/api/workflows/purge-attachment.mjs +34 -1
- package/dist/exports/cli/api/workflows/purge-audit-logs.mjs +47 -1
- package/dist/exports/cli/api/workflows/purge-unattached-blobs.mjs +46 -1
- package/dist/exports/cli/api/workflows/track-db-changes.mjs +110 -1
- package/dist/exports/cli/command.d.mts +2 -0
- package/dist/exports/cli/command.mjs +43 -1
- package/dist/exports/cli/constants.mjs +23 -1
- package/dist/exports/cli/index.mjs +3 -1
- package/dist/exports/devtools/index.js +4 -1
- package/dist/exports/tests/api/auth.d.mts +4 -0
- package/dist/exports/tests/api/cache.d.mts +2 -2
- package/dist/exports/tests/api/middleware/i18n.mjs +1 -1
- package/dist/exports/tests/api/middleware/youch-handler.mjs +1 -1
- package/dist/exports/tests/api/openapi.mjs +1 -1
- package/dist/exports/tests/api/server.mjs +1 -1
- package/dist/exports/tests/api/storage.d.mts +4 -4
- package/dist/exports/tests/constants.mjs +1 -1
- package/dist/exports/vendors/date.js +1 -1
- package/dist/exports/vendors/toolkit.js +1 -1
- package/dist/exports/vendors/zod.js +1 -1
- package/dist/exports/vitest/globals.mjs +1 -1
- package/dist/exports/web/auth.js +75 -1
- package/dist/exports/web/i18n.js +45 -1
- package/dist/exports/web/index.js +8 -1
- package/package.json +19 -18
- package/dist/bin/auth-schema-Va0CYicu.mjs +0 -2
- package/dist/bin/event-8JibGFH_.mjs +0 -2
- package/dist/bin/extract-blob-metadata-DjPfHtQ2.mjs +0 -2
- package/dist/bin/generate-image-variant-D5VDFyWj.mjs +0 -2
- package/dist/bin/generate-preview-Dssw7w5U.mjs +0 -2
- package/dist/bin/purge-attachment-BBPzIxwt.mjs +0 -2
- package/dist/bin/purge-audit-logs-BeZy3IFM.mjs +0 -2
- package/dist/bin/track-db-changes-CFykw_YO.mjs +0 -2
- package/dist/bin/workflow-BNUZrj4F.mjs +0 -2
- package/dist/bin/youch-handler-BadUgHb0.mjs +0 -2
|
@@ -1 +1,28 @@
|
|
|
1
|
-
import{defineRedisClient
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 };
|