@fitzzero/quickdraw-core 3.5.0 → 3.7.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 (33) hide show
  1. package/README.md +113 -64
  2. package/dist/client/index.d.ts +2 -2
  3. package/dist/client/index.js +61 -66
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/client/testing.d.ts +1 -1
  6. package/dist/client/testing.js.map +1 -1
  7. package/dist/server/express/index.d.ts +37 -0
  8. package/dist/server/express/index.js +60 -0
  9. package/dist/server/express/index.js.map +1 -0
  10. package/dist/server/index.d.ts +227 -1
  11. package/dist/server/index.js +400 -142
  12. package/dist/server/index.js.map +1 -1
  13. package/dist/server/testing-prisma.d.ts +101 -0
  14. package/dist/server/testing-prisma.js +172 -0
  15. package/dist/server/testing-prisma.js.map +1 -0
  16. package/dist/server/testing.js +5 -22
  17. package/dist/server/testing.js.map +1 -1
  18. package/dist/shared/index.js.map +1 -1
  19. package/dist/{types-BJj94_XW.d.ts → types-B1z5aXdR.d.ts} +11 -0
  20. package/eslint-config/index.mjs +1 -2
  21. package/eslint-plugin-quickdraw/index.mjs +15 -7
  22. package/eslint-plugin-quickdraw/rules/no-cross-service-mutations.mjs +31 -32
  23. package/eslint-plugin-quickdraw/rules/no-direct-prisma-mutations.mjs +17 -18
  24. package/eslint-plugin-quickdraw/rules/no-raw-button-strings.mjs +52 -0
  25. package/eslint-plugin-quickdraw/rules/no-raw-service-room-string.mjs +140 -0
  26. package/eslint-plugin-quickdraw/rules/no-raw-socket-emit.mjs +20 -22
  27. package/eslint-plugin-quickdraw/rules/no-raw-socket-on.mjs +17 -17
  28. package/eslint-plugin-quickdraw/rules/no-raw-tooltip-strings.mjs +59 -0
  29. package/eslint-plugin-quickdraw/rules/no-raw-typography-strings.mjs +52 -0
  30. package/eslint-plugin-quickdraw/rules/no-service-method-record.mjs +3 -3
  31. package/eslint-plugin-quickdraw/rules/no-unsafe-payload-cast.mjs +8 -8
  32. package/eslint-plugin-quickdraw/rules/require-zod-schema.mjs +15 -20
  33. package/package.json +74 -50
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Prisma test-database helpers (dual-mode: real PostgreSQL or PGlite).
3
+ *
4
+ * Exported as `@fitzzero/quickdraw-core/server/testing/prisma`.
5
+ *
6
+ * Real-PostgreSQL mode: a template database is migrated once per run, then
7
+ * per-worker databases are cloned from it (`CREATE DATABASE … TEMPLATE …`) so
8
+ * vitest workers never share state. PGlite mode (no TEST_DATABASE_URL):
9
+ * migrations run once into an in-memory PGlite instance whose data dir is
10
+ * dumped to a gzip cache keyed by a migrations fingerprint — later test
11
+ * processes boot from the dump in milliseconds with no PostgreSQL at all.
12
+ *
13
+ * `pg` and `@electric-sql/pglite` are optional peers, loaded via dynamic
14
+ * import only inside the mode that needs them. App-specific pieces (the
15
+ * generated PrismaClient, adapter choice, seed helpers) stay in the app.
16
+ */
17
+ interface RawSqlClient {
18
+ $executeRawUnsafe(sql: string): Promise<unknown>;
19
+ }
20
+ /**
21
+ * Reset a test database by truncating all public tables.
22
+ *
23
+ * Dynamic TRUNCATE discovers all tables so it never falls out of sync with
24
+ * schema changes. Retries on deadlock (surfaced by Prisma as P2010), which can
25
+ * occur when parallel test files within the same fork race on TRUNCATE vs
26
+ * active queries.
27
+ */
28
+ declare function resetDatabase(client: RawSqlClient): Promise<void>;
29
+ /**
30
+ * Rewrite a database URL to point at a per-worker database
31
+ * (e.g. ".../app_test" → ".../test_worker_3").
32
+ */
33
+ declare function workerDatabaseUrl(baseUrl: string, poolId: string, prefix?: string): string;
34
+ /**
35
+ * Hash of all migration.sql contents — cache key for the PGlite template.
36
+ */
37
+ declare function computeMigrationsFingerprint(migrationsDir: string): string;
38
+ /** All migration.sql contents in execution order. */
39
+ declare function readMigrationSql(migrationsDir: string): string[];
40
+ interface PgliteTemplateOptions {
41
+ /** Absolute path to the Prisma migrations directory. */
42
+ migrationsDir: string;
43
+ /** Cache directory for the template dump (e.g. node_modules/.cache). */
44
+ cacheDir: string;
45
+ /** Base name for the cache files (e.g. "myapp-test-template"). */
46
+ templateName: string;
47
+ }
48
+ interface PgliteTemplatePaths {
49
+ templatePath: string;
50
+ fingerprintPath: string;
51
+ }
52
+ /** Resolve the cache file locations for a PGlite template. */
53
+ declare function pgliteTemplatePaths(options: PgliteTemplateOptions): PgliteTemplatePaths;
54
+ /**
55
+ * Build (or reuse) the gzip'd PGlite data-dir template with all migrations
56
+ * applied. Returns the template path; skips the build when the cached
57
+ * fingerprint matches the current migrations.
58
+ */
59
+ declare function buildPgliteTemplate(options: PgliteTemplateOptions): Promise<{
60
+ templatePath: string;
61
+ rebuilt: boolean;
62
+ }>;
63
+ interface PostgresWorkerOptions {
64
+ /** Connection URL whose database segment names the template database. */
65
+ testDatabaseUrl: string;
66
+ /** Template database name (e.g. "myapp_test"). */
67
+ templateDbName: string;
68
+ /** Absolute path to the Prisma migrations directory. */
69
+ migrationsDir: string;
70
+ /** Number of per-worker databases to clone. Default: 8. */
71
+ workerCount?: number;
72
+ /** Worker database name prefix. Default: "test_worker_". */
73
+ workerPrefix?: string;
74
+ /** Seconds to wait for PostgreSQL to accept connections. Default: 30. */
75
+ maxWaitSeconds?: number;
76
+ }
77
+ /**
78
+ * Prepare real-PostgreSQL test databases: wait for the server, ensure the
79
+ * template database exists, reset + migrate it, then clone one database per
80
+ * worker from the template.
81
+ */
82
+ declare function setupPostgresWorkerDatabases(options: PostgresWorkerOptions): Promise<void>;
83
+ interface PrismaTestGlobalSetupOptions {
84
+ /** Absolute path to the Prisma migrations directory. */
85
+ migrationsDir: string;
86
+ /** Template database name for real-PostgreSQL mode. */
87
+ templateDbName: string;
88
+ /** Cache name for the PGlite template dump (e.g. "myapp-test-template"). */
89
+ templateName: string;
90
+ /** Cache directory. Default: <cwd>/node_modules/.cache. */
91
+ cacheDir?: string;
92
+ /** Per-worker database count for real-PostgreSQL mode. Default: 8. */
93
+ workerCount?: number;
94
+ }
95
+ /**
96
+ * Build a vitest globalSetup function that prepares the test databases.
97
+ * Picks real-PostgreSQL mode when TEST_DATABASE_URL is set, PGlite otherwise.
98
+ */
99
+ declare function createPrismaTestGlobalSetup(options: PrismaTestGlobalSetupOptions): () => Promise<void>;
100
+
101
+ export { type PgliteTemplateOptions, type PgliteTemplatePaths, type PostgresWorkerOptions, type PrismaTestGlobalSetupOptions, type RawSqlClient, buildPgliteTemplate, computeMigrationsFingerprint, createPrismaTestGlobalSetup, pgliteTemplatePaths, readMigrationSql, resetDatabase, setupPostgresWorkerDatabases, workerDatabaseUrl };
@@ -0,0 +1,172 @@
1
+ import { createHash } from 'crypto';
2
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
3
+ import { resolve } from 'path';
4
+
5
+ // src/server/testing-prisma.ts
6
+ async function resetDatabase(client) {
7
+ for (let attempt = 0; attempt < 3; attempt++) {
8
+ try {
9
+ await client.$executeRawUnsafe(`
10
+ DO $$
11
+ DECLARE _tables TEXT;
12
+ BEGIN
13
+ SELECT string_agg('"' || tablename || '"', ', ')
14
+ INTO _tables
15
+ FROM pg_tables
16
+ WHERE schemaname = 'public' AND tablename != '_prisma_migrations';
17
+ IF _tables IS NOT NULL THEN
18
+ EXECUTE 'TRUNCATE TABLE ' || _tables || ' CASCADE';
19
+ END IF;
20
+ END $$
21
+ `);
22
+ return;
23
+ } catch (error) {
24
+ const isDeadlock = error instanceof Error && "code" in error && error.code === "P2010";
25
+ if (!isDeadlock || attempt === 2) throw error;
26
+ await new Promise((r) => {
27
+ setTimeout(r, 50 * (attempt + 1));
28
+ });
29
+ }
30
+ }
31
+ }
32
+ function workerDatabaseUrl(baseUrl, poolId, prefix = "test_worker_") {
33
+ return baseUrl.replace(/\/[^/?]+(\?|$)/, `/${prefix}${poolId}$1`);
34
+ }
35
+ function migrationDirNames(migrationsDir) {
36
+ return readdirSync(migrationsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
37
+ }
38
+ function computeMigrationsFingerprint(migrationsDir) {
39
+ const hash = createHash("sha256");
40
+ for (const dir of migrationDirNames(migrationsDir)) {
41
+ const sqlPath = resolve(migrationsDir, dir, "migration.sql");
42
+ if (existsSync(sqlPath)) {
43
+ hash.update(readFileSync(sqlPath));
44
+ }
45
+ }
46
+ return hash.digest("hex");
47
+ }
48
+ function readMigrationSql(migrationsDir) {
49
+ const sqls = [];
50
+ for (const dir of migrationDirNames(migrationsDir)) {
51
+ const sqlPath = resolve(migrationsDir, dir, "migration.sql");
52
+ if (existsSync(sqlPath)) {
53
+ sqls.push(readFileSync(sqlPath, "utf-8"));
54
+ }
55
+ }
56
+ return sqls;
57
+ }
58
+ function pgliteTemplatePaths(options) {
59
+ return {
60
+ templatePath: resolve(options.cacheDir, `${options.templateName}.tar.gz`),
61
+ fingerprintPath: resolve(options.cacheDir, `${options.templateName}.fingerprint`)
62
+ };
63
+ }
64
+ async function buildPgliteTemplate(options) {
65
+ const { templatePath, fingerprintPath } = pgliteTemplatePaths(options);
66
+ const fingerprint = computeMigrationsFingerprint(options.migrationsDir);
67
+ mkdirSync(options.cacheDir, { recursive: true });
68
+ if (existsSync(templatePath) && existsSync(fingerprintPath)) {
69
+ const cached = readFileSync(fingerprintPath, "utf-8").trim();
70
+ if (cached === fingerprint) {
71
+ return { templatePath, rebuilt: false };
72
+ }
73
+ }
74
+ const { PGlite } = await import('@electric-sql/pglite');
75
+ const pg = new PGlite();
76
+ for (const sql of readMigrationSql(options.migrationsDir)) {
77
+ await pg.exec(sql);
78
+ }
79
+ const dump = await pg.dumpDataDir("gzip");
80
+ const buffer = Buffer.from(await dump.arrayBuffer());
81
+ writeFileSync(templatePath, buffer);
82
+ writeFileSync(fingerprintPath, fingerprint);
83
+ await pg.close();
84
+ return { templatePath, rebuilt: true };
85
+ }
86
+ function withDatabase(url, database) {
87
+ return url.replace(/\/[^/?]+(\?|$)/, `/${database}$1`);
88
+ }
89
+ async function setupPostgresWorkerDatabases(options) {
90
+ const { default: pg } = await import('pg');
91
+ const workerCount = options.workerCount ?? 8;
92
+ const workerPrefix = options.workerPrefix ?? "test_worker_";
93
+ const maxAttempts = options.maxWaitSeconds ?? 30;
94
+ const adminUrl = withDatabase(options.testDatabaseUrl, "postgres");
95
+ const templateUrl = withDatabase(options.testDatabaseUrl, options.templateDbName);
96
+ for (let i = 0; i < maxAttempts; i++) {
97
+ try {
98
+ const client2 = new pg.Client({ connectionString: adminUrl });
99
+ await client2.connect();
100
+ await client2.end();
101
+ break;
102
+ } catch {
103
+ if (i === maxAttempts - 1) {
104
+ throw new Error(`PostgreSQL not ready after ${maxAttempts}s`);
105
+ }
106
+ await new Promise((res) => {
107
+ setTimeout(res, 1e3);
108
+ });
109
+ }
110
+ }
111
+ {
112
+ const client2 = new pg.Client({ connectionString: adminUrl });
113
+ await client2.connect();
114
+ try {
115
+ const res = await client2.query("SELECT 1 FROM pg_database WHERE datname = $1", [
116
+ options.templateDbName
117
+ ]);
118
+ if (res.rowCount === 0) {
119
+ await client2.query(`CREATE DATABASE ${options.templateDbName}`);
120
+ }
121
+ } finally {
122
+ await client2.end();
123
+ }
124
+ }
125
+ const client = new pg.Client({ connectionString: templateUrl });
126
+ await client.connect();
127
+ try {
128
+ await client.query(
129
+ `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`,
130
+ [options.templateDbName]
131
+ );
132
+ await client.query("DROP SCHEMA public CASCADE");
133
+ await client.query("CREATE SCHEMA public");
134
+ for (const sql of readMigrationSql(options.migrationsDir)) {
135
+ await client.query(sql);
136
+ }
137
+ for (let i = 0; i < workerCount; i++) {
138
+ const workerDb = `${workerPrefix}${i}`;
139
+ await client.query(
140
+ `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`,
141
+ [workerDb]
142
+ );
143
+ await client.query(`DROP DATABASE IF EXISTS ${workerDb}`);
144
+ await client.query(`CREATE DATABASE ${workerDb} TEMPLATE ${options.templateDbName}`);
145
+ }
146
+ } finally {
147
+ await client.end();
148
+ }
149
+ }
150
+ function createPrismaTestGlobalSetup(options) {
151
+ return async () => {
152
+ const testDatabaseUrl = process.env.TEST_DATABASE_URL;
153
+ if (testDatabaseUrl) {
154
+ await setupPostgresWorkerDatabases({
155
+ testDatabaseUrl,
156
+ templateDbName: options.templateDbName,
157
+ migrationsDir: options.migrationsDir,
158
+ workerCount: options.workerCount
159
+ });
160
+ } else {
161
+ await buildPgliteTemplate({
162
+ migrationsDir: options.migrationsDir,
163
+ cacheDir: options.cacheDir ?? resolve(process.cwd(), "node_modules/.cache"),
164
+ templateName: options.templateName
165
+ });
166
+ }
167
+ };
168
+ }
169
+
170
+ export { buildPgliteTemplate, computeMigrationsFingerprint, createPrismaTestGlobalSetup, pgliteTemplatePaths, readMigrationSql, resetDatabase, setupPostgresWorkerDatabases, workerDatabaseUrl };
171
+ //# sourceMappingURL=testing-prisma.js.map
172
+ //# sourceMappingURL=testing-prisma.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/testing-prisma.ts"],"names":["client"],"mappings":"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","file":"testing-prisma.js","sourcesContent":["/**\n * Prisma test-database helpers (dual-mode: real PostgreSQL or PGlite).\n *\n * Exported as `@fitzzero/quickdraw-core/server/testing/prisma`.\n *\n * Real-PostgreSQL mode: a template database is migrated once per run, then\n * per-worker databases are cloned from it (`CREATE DATABASE … TEMPLATE …`) so\n * vitest workers never share state. PGlite mode (no TEST_DATABASE_URL):\n * migrations run once into an in-memory PGlite instance whose data dir is\n * dumped to a gzip cache keyed by a migrations fingerprint — later test\n * processes boot from the dump in milliseconds with no PostgreSQL at all.\n *\n * `pg` and `@electric-sql/pglite` are optional peers, loaded via dynamic\n * import only inside the mode that needs them. App-specific pieces (the\n * generated PrismaClient, adapter choice, seed helpers) stay in the app.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nexport interface RawSqlClient {\n $executeRawUnsafe(sql: string): Promise<unknown>;\n}\n\n/**\n * Reset a test database by truncating all public tables.\n *\n * Dynamic TRUNCATE discovers all tables so it never falls out of sync with\n * schema changes. Retries on deadlock (surfaced by Prisma as P2010), which can\n * occur when parallel test files within the same fork race on TRUNCATE vs\n * active queries.\n */\nexport async function resetDatabase(client: RawSqlClient): Promise<void> {\n for (let attempt = 0; attempt < 3; attempt++) {\n try {\n await client.$executeRawUnsafe(`\n DO $$\n DECLARE _tables TEXT;\n BEGIN\n SELECT string_agg('\"' || tablename || '\"', ', ')\n INTO _tables\n FROM pg_tables\n WHERE schemaname = 'public' AND tablename != '_prisma_migrations';\n IF _tables IS NOT NULL THEN\n EXECUTE 'TRUNCATE TABLE ' || _tables || ' CASCADE';\n END IF;\n END $$\n `);\n return;\n } catch (error: unknown) {\n const isDeadlock =\n error instanceof Error && \"code\" in error && (error as { code: string }).code === \"P2010\";\n if (!isDeadlock || attempt === 2) throw error;\n await new Promise<void>((r) => {\n setTimeout(r, 50 * (attempt + 1));\n });\n }\n }\n}\n\n/**\n * Rewrite a database URL to point at a per-worker database\n * (e.g. \".../app_test\" → \".../test_worker_3\").\n */\nexport function workerDatabaseUrl(\n baseUrl: string,\n poolId: string,\n prefix: string = \"test_worker_\",\n): string {\n return baseUrl.replace(/\\/[^/?]+(\\?|$)/, `/${prefix}${poolId}$1`);\n}\n\n/** Sorted migration directory names under a Prisma migrations dir. */\nfunction migrationDirNames(migrationsDir: string): string[] {\n return readdirSync(migrationsDir, { withFileTypes: true })\n .filter((d) => d.isDirectory())\n .map((d) => d.name)\n .sort();\n}\n\n/**\n * Hash of all migration.sql contents — cache key for the PGlite template.\n */\nexport function computeMigrationsFingerprint(migrationsDir: string): string {\n const hash = createHash(\"sha256\");\n for (const dir of migrationDirNames(migrationsDir)) {\n const sqlPath = resolve(migrationsDir, dir, \"migration.sql\");\n if (existsSync(sqlPath)) {\n hash.update(readFileSync(sqlPath));\n }\n }\n return hash.digest(\"hex\");\n}\n\n/** All migration.sql contents in execution order. */\nexport function readMigrationSql(migrationsDir: string): string[] {\n const sqls: string[] = [];\n for (const dir of migrationDirNames(migrationsDir)) {\n const sqlPath = resolve(migrationsDir, dir, \"migration.sql\");\n if (existsSync(sqlPath)) {\n sqls.push(readFileSync(sqlPath, \"utf-8\"));\n }\n }\n return sqls;\n}\n\nexport interface PgliteTemplateOptions {\n /** Absolute path to the Prisma migrations directory. */\n migrationsDir: string;\n /** Cache directory for the template dump (e.g. node_modules/.cache). */\n cacheDir: string;\n /** Base name for the cache files (e.g. \"myapp-test-template\"). */\n templateName: string;\n}\n\nexport interface PgliteTemplatePaths {\n templatePath: string;\n fingerprintPath: string;\n}\n\n/** Resolve the cache file locations for a PGlite template. */\nexport function pgliteTemplatePaths(options: PgliteTemplateOptions): PgliteTemplatePaths {\n return {\n templatePath: resolve(options.cacheDir, `${options.templateName}.tar.gz`),\n fingerprintPath: resolve(options.cacheDir, `${options.templateName}.fingerprint`),\n };\n}\n\n/**\n * Build (or reuse) the gzip'd PGlite data-dir template with all migrations\n * applied. Returns the template path; skips the build when the cached\n * fingerprint matches the current migrations.\n */\nexport async function buildPgliteTemplate(\n options: PgliteTemplateOptions,\n): Promise<{ templatePath: string; rebuilt: boolean }> {\n const { templatePath, fingerprintPath } = pgliteTemplatePaths(options);\n const fingerprint = computeMigrationsFingerprint(options.migrationsDir);\n\n mkdirSync(options.cacheDir, { recursive: true });\n\n if (existsSync(templatePath) && existsSync(fingerprintPath)) {\n const cached = readFileSync(fingerprintPath, \"utf-8\").trim();\n if (cached === fingerprint) {\n return { templatePath, rebuilt: false };\n }\n }\n\n const { PGlite } = await import(\"@electric-sql/pglite\");\n const pg = new PGlite();\n\n for (const sql of readMigrationSql(options.migrationsDir)) {\n await pg.exec(sql);\n }\n\n const dump = await pg.dumpDataDir(\"gzip\");\n const buffer = Buffer.from(await dump.arrayBuffer());\n writeFileSync(templatePath, buffer);\n writeFileSync(fingerprintPath, fingerprint);\n\n await pg.close();\n return { templatePath, rebuilt: true };\n}\n\nexport interface PostgresWorkerOptions {\n /** Connection URL whose database segment names the template database. */\n testDatabaseUrl: string;\n /** Template database name (e.g. \"myapp_test\"). */\n templateDbName: string;\n /** Absolute path to the Prisma migrations directory. */\n migrationsDir: string;\n /** Number of per-worker databases to clone. Default: 8. */\n workerCount?: number;\n /** Worker database name prefix. Default: \"test_worker_\". */\n workerPrefix?: string;\n /** Seconds to wait for PostgreSQL to accept connections. Default: 30. */\n maxWaitSeconds?: number;\n}\n\n/** Swap the database segment of a connection URL. */\nfunction withDatabase(url: string, database: string): string {\n return url.replace(/\\/[^/?]+(\\?|$)/, `/${database}$1`);\n}\n\n/**\n * Prepare real-PostgreSQL test databases: wait for the server, ensure the\n * template database exists, reset + migrate it, then clone one database per\n * worker from the template.\n */\nexport async function setupPostgresWorkerDatabases(options: PostgresWorkerOptions): Promise<void> {\n const { default: pg } = await import(\"pg\");\n const workerCount = options.workerCount ?? 8;\n const workerPrefix = options.workerPrefix ?? \"test_worker_\";\n const maxAttempts = options.maxWaitSeconds ?? 30;\n const adminUrl = withDatabase(options.testDatabaseUrl, \"postgres\");\n const templateUrl = withDatabase(options.testDatabaseUrl, options.templateDbName);\n\n // Wait for PostgreSQL to accept connections — guards against timing races\n // when tests start before a sidecar container is fully ready.\n for (let i = 0; i < maxAttempts; i++) {\n try {\n const client = new pg.Client({ connectionString: adminUrl });\n await client.connect();\n await client.end();\n break;\n } catch {\n if (i === maxAttempts - 1) {\n throw new Error(`PostgreSQL not ready after ${maxAttempts}s`);\n }\n await new Promise<void>((res) => {\n setTimeout(res, 1000);\n });\n }\n }\n\n // Ensure the template database exists (self-heals fresh environments).\n {\n const client = new pg.Client({ connectionString: adminUrl });\n await client.connect();\n try {\n const res = await client.query(\"SELECT 1 FROM pg_database WHERE datname = $1\", [\n options.templateDbName,\n ]);\n if (res.rowCount === 0) {\n await client.query(`CREATE DATABASE ${options.templateDbName}`);\n }\n } finally {\n await client.end();\n }\n }\n\n // Reset + migrate the template, then clone per-worker databases from it.\n const client = new pg.Client({ connectionString: templateUrl });\n await client.connect();\n try {\n await client.query(\n `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`,\n [options.templateDbName],\n );\n\n await client.query(\"DROP SCHEMA public CASCADE\");\n await client.query(\"CREATE SCHEMA public\");\n\n for (const sql of readMigrationSql(options.migrationsDir)) {\n await client.query(sql);\n }\n\n for (let i = 0; i < workerCount; i++) {\n const workerDb = `${workerPrefix}${i}`;\n await client.query(\n `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`,\n [workerDb],\n );\n await client.query(`DROP DATABASE IF EXISTS ${workerDb}`);\n await client.query(`CREATE DATABASE ${workerDb} TEMPLATE ${options.templateDbName}`);\n }\n } finally {\n await client.end();\n }\n}\n\nexport interface PrismaTestGlobalSetupOptions {\n /** Absolute path to the Prisma migrations directory. */\n migrationsDir: string;\n /** Template database name for real-PostgreSQL mode. */\n templateDbName: string;\n /** Cache name for the PGlite template dump (e.g. \"myapp-test-template\"). */\n templateName: string;\n /** Cache directory. Default: <cwd>/node_modules/.cache. */\n cacheDir?: string;\n /** Per-worker database count for real-PostgreSQL mode. Default: 8. */\n workerCount?: number;\n}\n\n/**\n * Build a vitest globalSetup function that prepares the test databases.\n * Picks real-PostgreSQL mode when TEST_DATABASE_URL is set, PGlite otherwise.\n */\nexport function createPrismaTestGlobalSetup(\n options: PrismaTestGlobalSetupOptions,\n): () => Promise<void> {\n return async () => {\n const testDatabaseUrl = process.env.TEST_DATABASE_URL;\n if (testDatabaseUrl) {\n await setupPostgresWorkerDatabases({\n testDatabaseUrl,\n templateDbName: options.templateDbName,\n migrationsDir: options.migrationsDir,\n workerCount: options.workerCount,\n });\n } else {\n await buildPgliteTemplate({\n migrationsDir: options.migrationsDir,\n cacheDir: options.cacheDir ?? resolve(process.cwd(), \"node_modules/.cache\"),\n templateName: options.templateName,\n });\n }\n };\n}\n"]}
@@ -59,21 +59,9 @@ var ServiceRegistry = class {
59
59
  this.registerMethodListener(socket, eventName, method, service);
60
60
  }
61
61
  for (const [serviceName, service] of this.services) {
62
- this.registerSubscriptionListener(
63
- socket,
64
- `${serviceName}:subscribe`,
65
- service
66
- );
67
- this.registerBatchSubscriptionListener(
68
- socket,
69
- `${serviceName}:batchSubscribe`,
70
- service
71
- );
72
- this.registerUnsubscriptionListener(
73
- socket,
74
- `${serviceName}:unsubscribe`,
75
- service
76
- );
62
+ this.registerSubscriptionListener(socket, `${serviceName}:subscribe`, service);
63
+ this.registerBatchSubscriptionListener(socket, `${serviceName}:batchSubscribe`, service);
64
+ this.registerUnsubscriptionListener(socket, `${serviceName}:unsubscribe`, service);
77
65
  }
78
66
  }
79
67
  /**
@@ -206,10 +194,7 @@ var ServiceRegistry = class {
206
194
  if (this.logPayloads) {
207
195
  logContext.payload = payload;
208
196
  }
209
- this.logger.error(
210
- `User ${userIdShort} ${serviceName}.${methodName} -> fail`,
211
- logContext
212
- );
197
+ this.logger.error(`User ${userIdShort} ${serviceName}.${methodName} -> fail`, logContext);
213
198
  const errorResponse = {
214
199
  success: false,
215
200
  error: error instanceof Error ? error.message : "Internal error",
@@ -407,9 +392,7 @@ function createQuickdrawServer(options) {
407
392
  });
408
393
  httpServer.listen(options.port, "0.0.0.0", () => {
409
394
  serverLogger.info(`Server listening on port ${options.port}`);
410
- serverLogger.info(
411
- `Registered services: ${registry.getServices().join(", ")}`
412
- );
395
+ serverLogger.info(`Registered services: ${registry.getServices().join(", ")}`);
413
396
  });
414
397
  const shutdown = async (signal) => {
415
398
  serverLogger.info(`Received ${signal}, starting graceful shutdown...`);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shared/types.ts","../../src/server/ServiceRegistry.ts","../../src/server/createServer.ts","../../src/server/testing.ts"],"names":["result","SocketIOServer","ioClient"],"mappings":"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","file":"testing.js","sourcesContent":["import type { z } from \"zod\";\n\n// ============================================================================\n// Access Control Types\n// ============================================================================\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n\n// ============================================================================\n// Entity and User Interfaces for ACL\n// ============================================================================\n\n/**\n * Interface for entities that support entry-level ACL stored as JSON.\n *\n * Use this pattern when:\n * - You want simple ACL without a separate membership table\n * - Access patterns are straightforward (read/write permissions)\n * - You don't need to query \"all entities user X can access\" efficiently\n *\n * The default `checkEntryACL` in BaseService expects entities to have this shape.\n *\n * @example\n * ```prisma\n * model Document {\n * id String @id @default(cuid())\n * title String\n * acl Json? // Stores ACL array\n * }\n * ```\n *\n * @example\n * ```typescript\n * class DocumentService extends BaseService<Document & ACLEntity, ...> {\n * constructor(prisma: PrismaClient) {\n * super({ serviceName: 'documentService', hasEntryACL: true });\n * this.setDelegate(prisma.document);\n * // Default checkEntryACL will work automatically\n * }\n * }\n * ```\n */\nexport interface ACLEntity {\n id: string;\n acl?: ACL | null;\n}\n\n/**\n * Interface for users with service-level access control.\n *\n * The `serviceAccess` field maps service names to access levels, granting\n * blanket permissions across all entries in that service.\n *\n * @example\n * ```prisma\n * model User {\n * id String @id @default(cuid())\n * serviceAccess Json? // { \"chatService\": \"Admin\", \"documentService\": \"Read\" }\n * }\n * ```\n *\n * @example\n * ```typescript\n * // User with Admin access to chatService can perform any operation\n * // on any chat, regardless of entry-level ACL\n * const user: QuickdrawUser = {\n * id: \"user-123\",\n * serviceAccess: { chatService: \"Admin\" }\n * };\n * ```\n */\nexport interface QuickdrawUser {\n id: string;\n serviceAccess?: Record<string, AccessLevel> | null;\n}\n\n// ============================================================================\n// Service Response Types\n// ============================================================================\n\nexport type ServiceResponse<T = unknown> =\n | { success: true; data: T }\n | { success: false; error: string; code?: number };\n\n// ============================================================================\n// Service Method Definition Types\n// ============================================================================\n\n/**\n * Defines the shape of a service method including its payload and response types.\n * Used to create a type-safe contract between server and client.\n */\nexport interface ServiceMethodDefinition<\n TPayload = unknown,\n TResponse = unknown,\n> {\n name: string;\n access: AccessLevel;\n handler: (\n payload: TPayload,\n context: ServiceMethodContext\n ) => Promise<TResponse>;\n schema?: z.ZodType<TPayload>;\n resolveEntryId?: (payload: TPayload) => string | null;\n}\n\n/**\n * Context provided to service method handlers\n */\nexport interface ServiceMethodContext {\n userId: string | undefined;\n socketId: string;\n serviceAccess: Record<string, AccessLevel>;\n}\n\n/**\n * Type helper for defining service method maps.\n * Each service should define its methods using this structure.\n *\n * @example\n * ```typescript\n * type ChatServiceMethods = ServiceMethodMap<{\n * createChat: {\n * payload: { title: string };\n * response: { id: string };\n * };\n * updateTitle: {\n * payload: { id: string; title: string };\n * response: { id: string; title: string };\n * };\n * }>;\n * ```\n */\nexport type ServiceMethodMap<\n T extends Record<string, { payload: unknown; response: unknown }>,\n> = T;\n\n// ============================================================================\n// Admin Method Types (Generic shapes for all services)\n// ============================================================================\n\nexport type AdminListPayload = {\n page?: number;\n pageSize?: number;\n sort?: {\n field?: string;\n direction?: \"asc\" | \"desc\";\n };\n filter?: {\n id?: string;\n ids?: string[];\n createdAfter?: string;\n createdBefore?: string;\n updatedAfter?: string;\n updatedBefore?: string;\n } & Record<string, unknown>;\n};\n\nexport type AdminListResponse<T> = {\n rows: T[];\n page: number;\n pageSize: number;\n total: number;\n};\n\nexport type AdminGetPayload = { id: string };\n\nexport type AdminCreatePayload<TInsert> = { data: Partial<TInsert> };\n\nexport type AdminUpdatePayload<TInsert> = {\n id: string;\n data: Partial<TInsert>;\n};\n\nexport type AdminDeletePayload = { id: string };\nexport type AdminDeleteResponse = { id: string; deleted: true };\n\nexport type AdminSetEntryACLPayload = {\n id: string;\n acl: ACL;\n};\n\nexport type AdminGetSubscribersPayload = { id: string };\nexport type AdminGetSubscribersResponse = {\n id: string;\n subscribers: Array<{ socketId: string; userId?: string }>;\n};\n\nexport type AdminReemitPayload = { id: string };\nexport type AdminReemitResponse = { emitted: boolean };\n\nexport type AdminUnsubscribeAllPayload = { id: string };\nexport type AdminUnsubscribeAllResponse = { id: string; unsubscribed: number };\n\n// ============================================================================\n// Admin Field Configuration Types\n// ============================================================================\n\n/**\n * Supported field types for admin UI generation.\n */\nexport type AdminFieldType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"enum\"\n | \"json\"\n | \"relation\";\n\n/**\n * Configuration for a single field in the admin UI.\n * Derived from Zod schemas with optional overrides.\n */\nexport interface AdminFieldConfig {\n /** Field name (property key on the entity) */\n name: string;\n /** Field type for rendering appropriate input */\n type: AdminFieldType;\n /** Human-readable label */\n label: string;\n /** Whether the field is required for creation */\n required: boolean;\n /** Whether the field can be edited (false for id, createdAt, etc.) */\n editable: boolean;\n /** Whether to show this field as a table column */\n showInTable: boolean;\n /** Whether the table can be sorted by this field */\n sortable: boolean;\n /** For enum fields, the allowed values */\n enumValues?: string[];\n /** For relation fields, the related service name */\n relationService?: string;\n}\n\n/**\n * Service metadata for admin UI generation.\n * Returned by the adminMeta method.\n */\nexport interface AdminServiceMeta {\n /** Internal service name (e.g., \"chatService\") */\n serviceName: string;\n /** Human-readable display name (e.g., \"Chats\") */\n displayName: string;\n /** Field configurations derived from schema */\n fields: AdminFieldConfig[];\n}\n\n/**\n * Payload for adminMeta method (no parameters needed)\n */\nexport type AdminMetaPayload = Record<string, never>;\n\n/**\n * Response type for adminMeta method\n */\nexport type AdminMetaResponse = AdminServiceMeta;\n\n// ============================================================================\n// Subscription Types\n// ============================================================================\n\nexport type SubscribePayload = {\n entryId: string;\n requiredLevel?: AccessLevel;\n};\n\nexport type UnsubscribePayload = {\n entryId: string;\n};\n\n// ============================================================================\n// Utility Types\n// ============================================================================\n\n/**\n * Extracts payload type from a service method definition\n */\nexport type ExtractPayload<T> = T extends { payload: infer P } ? P : never;\n\n/**\n * Extracts response type from a service method definition\n */\nexport type ExtractResponse<T> = T extends { response: infer R } ? R : never;\n\n/**\n * Logger interface compatible with Winston and other loggers\n */\nexport interface Logger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n debug(message: string, meta?: Record<string, unknown>): void;\n child(options: Record<string, unknown>): Logger;\n}\n\n/**\n * Default console logger implementation\n */\nexport const consoleLogger: Logger = {\n info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? \"\"),\n warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[ERROR] ${message}`, meta ?? \"\"),\n debug: (message, meta) => console.debug(`[DEBUG] ${message}`, meta ?? \"\"),\n child: () => consoleLogger,\n};\n","import type { Server as SocketIOServer } from \"socket.io\";\nimport type {\n ServiceResponse,\n ServiceMethodDefinition,\n Logger,\n} from \"../shared/types\";\nimport { consoleLogger } from \"../shared/types\";\nimport type {\n QuickdrawSocket,\n BaseServiceInstance,\n ServiceRegistryInstance,\n} from \"./types\";\n\n/**\n * Central registry for auto-discovering and registering service methods as Socket.io events.\n *\n * Features:\n * - Auto-discovers public methods from registered services\n * - Wires methods to Socket.io events with standardized naming\n * - Handles subscription lifecycle\n * - Provides consistent error handling and logging\n * - Uses a single connection handler for all services (memory efficient)\n *\n * @example\n * ```typescript\n * const registry = new ServiceRegistry(io, { logger });\n * registry.registerService('chatService', chatService);\n * registry.registerService('userService', userService);\n * ```\n */\nexport interface ServiceRegistryOptions {\n logger?: Logger;\n /**\n * Automatic method logging configuration.\n * @default { enabled: true, logPayloads: false, logResponses: false }\n */\n methodLogging?: {\n enabled?: boolean;\n logPayloads?: boolean;\n logResponses?: boolean;\n };\n}\n\nexport class ServiceRegistry implements ServiceRegistryInstance {\n private readonly io: SocketIOServer;\n private readonly services: Map<string, BaseServiceInstance> = new Map();\n private readonly logger: Logger;\n private readonly methodLoggingEnabled: boolean;\n private readonly logPayloads: boolean;\n private readonly logResponses: boolean;\n private connectionHandlerInstalled = false;\n\n // Store method metadata for deferred registration\n private readonly methodRegistry: Map<\n string,\n {\n serviceName: string;\n method: ServiceMethodDefinition<unknown, unknown>;\n service: BaseServiceInstance;\n }\n > = new Map();\n\n constructor(io: SocketIOServer, options?: ServiceRegistryOptions) {\n this.io = io;\n this.logger =\n options?.logger?.child({ service: \"ServiceRegistry\" }) ??\n consoleLogger.child({ service: \"ServiceRegistry\" });\n \n // Method logging configuration (enabled by default)\n this.methodLoggingEnabled = options?.methodLogging?.enabled ?? true;\n this.logPayloads = options?.methodLogging?.logPayloads ?? false;\n this.logResponses = options?.methodLogging?.logResponses ?? false;\n\n if (this.methodLoggingEnabled) {\n this.logger.info(\"Automatic method logging enabled\", {\n logPayloads: this.logPayloads,\n logResponses: this.logResponses,\n });\n }\n }\n\n /**\n * Install the single connection handler that wires all registered services.\n * This is called lazily on first service registration.\n */\n private installConnectionHandler(): void {\n if (this.connectionHandlerInstalled) return;\n this.connectionHandlerInstalled = true;\n\n this.io.on(\"connection\", (socket) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n this.setupSocketListeners(quickdrawSocket);\n });\n\n this.logger.info(\"Installed single connection handler for all services\");\n }\n\n /**\n * Set up all event listeners for a newly connected socket.\n */\n private setupSocketListeners(socket: QuickdrawSocket): void {\n // Register method listeners for all registered services\n for (const [eventName, { method, service }] of this.methodRegistry) {\n this.registerMethodListener(socket, eventName, method, service);\n }\n\n // Register subscription/unsubscription listeners for all services\n for (const [serviceName, service] of this.services) {\n this.registerSubscriptionListener(\n socket,\n `${serviceName}:subscribe`,\n service\n );\n this.registerBatchSubscriptionListener(\n socket,\n `${serviceName}:batchSubscribe`,\n service\n );\n this.registerUnsubscriptionListener(\n socket,\n `${serviceName}:unsubscribe`,\n service\n );\n }\n }\n\n /**\n * Register a service and auto-discover its public methods.\n */\n public registerService(\n serviceName: string,\n service: BaseServiceInstance\n ): void {\n this.services.set(serviceName, service);\n\n // Pass io instance to service for room-based broadcasts\n if (service.setIo) {\n service.setIo(this.io);\n }\n\n this.logger.info(`Registered service: ${serviceName}`);\n\n // Auto-discover and store method metadata\n this.discoverServiceMethods(serviceName, service);\n\n // Ensure connection handler is installed\n this.installConnectionHandler();\n }\n\n /**\n * Discover and store all public methods from a service.\n */\n private discoverServiceMethods(\n serviceName: string,\n service: BaseServiceInstance\n ): void {\n const methods = service.getPublicMethods();\n\n for (const method of methods) {\n const eventName = `${serviceName}:${method.name}`;\n this.logger.info(`Registering socket event: ${eventName}`);\n\n // Store method metadata for deferred registration on connection\n this.methodRegistry.set(eventName, {\n serviceName,\n method,\n service,\n });\n }\n }\n\n /**\n * Register a method as a socket event listener.\n */\n private registerMethodListener(\n socket: QuickdrawSocket,\n eventName: string,\n method: ServiceMethodDefinition<unknown, unknown>,\n service: BaseServiceInstance\n ): void {\n socket.on(\n eventName,\n async (\n payload: unknown,\n callback?: (response: ServiceResponse<unknown>) => void\n ) => {\n const startedAt = Date.now();\n const [serviceName, methodName] = eventName.split(\":\");\n const userIdShort = socket.userId?.slice(0, 8) ?? \"anon\";\n\n try {\n // Authentication check\n if (!socket.userId && method.access !== \"Public\") {\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: \"Authentication required\",\n code: 401,\n };\n callback?.(errorResponse);\n return;\n }\n\n // Validate payload with Zod schema if provided\n let validatedPayload = payload;\n if (method.schema) {\n const result = method.schema.safeParse(payload);\n if (!result.success) {\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: `Validation error: ${result.error.message}`,\n code: 400,\n };\n callback?.(errorResponse);\n return;\n }\n validatedPayload = result.data;\n }\n\n // Resolve entry ID for access check\n let entryId: string | undefined;\n if (method.resolveEntryId) {\n entryId = method.resolveEntryId(validatedPayload) ?? undefined;\n } else if (\n validatedPayload &&\n typeof validatedPayload === \"object\" &&\n \"id\" in validatedPayload\n ) {\n const idValue = (validatedPayload as Record<string, unknown>).id;\n if (typeof idValue === \"string\") {\n entryId = idValue;\n }\n }\n\n // Check access\n await service.ensureAccessForMethod(method.access, socket, entryId);\n\n // Log start (if enabled)\n if (this.methodLoggingEnabled) {\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n serviceName,\n methodName,\n userId: socket.userId,\n socketId: socket.id,\n };\n if (this.logPayloads) {\n logContext.payload = validatedPayload;\n }\n this.logger.info(\n `User ${userIdShort} ${serviceName}.${methodName} -> start`,\n logContext\n );\n }\n\n // Execute handler\n const result = await method.handler(validatedPayload, {\n userId: socket.userId,\n socketId: socket.id,\n serviceAccess: socket.serviceAccess ?? {},\n });\n\n // Log success (if enabled)\n if (this.methodLoggingEnabled) {\n const durationMs = Date.now() - startedAt;\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n outcome: \"success\",\n durationMs,\n serviceName,\n methodName,\n userId: socket.userId,\n };\n if (this.logResponses) {\n logContext.response = result;\n }\n this.logger.info(\n `User ${userIdShort} ${serviceName}.${methodName} -> success`,\n logContext\n );\n }\n\n const successResponse: ServiceResponse<unknown> = {\n success: true,\n data: result,\n };\n callback?.(successResponse);\n } catch (error) {\n // Always log errors, even if methodLogging is disabled\n const durationMs = Date.now() - startedAt;\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n outcome: \"fail\",\n durationMs,\n serviceName,\n methodName,\n userId: socket.userId,\n socketId: socket.id,\n error: error instanceof Error ? error.message : \"Unknown error\",\n };\n if (this.logPayloads) {\n logContext.payload = payload;\n }\n this.logger.error(\n `User ${userIdShort} ${serviceName}.${methodName} -> fail`,\n logContext\n );\n\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: error instanceof Error ? error.message : \"Internal error\",\n code: 500,\n };\n callback?.(errorResponse);\n }\n }\n );\n }\n\n /**\n * Register subscription listener for a service.\n */\n private registerSubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance\n ): void {\n socket.on(\n eventName,\n async (\n payload: { entryId: string; requiredLevel?: string },\n callback?: (response: ServiceResponse<unknown>) => void\n ) => {\n try {\n if (!socket.userId) {\n callback?.({\n success: false,\n error: \"Authentication required\",\n code: 401,\n });\n return;\n }\n\n const data = await service.subscribe(\n payload.entryId,\n socket,\n (payload.requiredLevel as \"Read\" | \"Moderate\" | \"Admin\") ?? \"Read\"\n );\n\n if (data === null) {\n callback?.({\n success: false,\n error: \"Access denied or entry not found\",\n code: 403,\n });\n return;\n }\n\n callback?.({ success: true, data });\n } catch (error) {\n this.logger.error(`Error in subscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error:\n error instanceof Error ? error.message : \"Subscription failed\",\n });\n }\n }\n );\n }\n\n /**\n * Register batch subscription listener for a service.\n * Handles subscribing to multiple entities in a single event.\n */\n private registerBatchSubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance\n ): void {\n socket.on(\n eventName,\n async (\n payload: { entryIds: string[]; requiredLevel?: string },\n callback?: (\n response: ServiceResponse<Record<string, unknown>>\n ) => void\n ) => {\n try {\n if (!socket.userId) {\n callback?.({\n success: false,\n error: \"Authentication required\",\n code: 401,\n });\n return;\n }\n\n if (\n !Array.isArray(payload.entryIds) ||\n payload.entryIds.length === 0\n ) {\n callback?.({\n success: false,\n error: \"entryIds must be a non-empty array\",\n code: 400,\n });\n return;\n }\n\n const data = await service.batchSubscribe(\n payload.entryIds,\n socket,\n (payload.requiredLevel as \"Read\" | \"Moderate\" | \"Admin\") ?? \"Read\"\n );\n\n callback?.({ success: true, data });\n } catch (error) {\n this.logger.error(`Error in batch subscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error:\n error instanceof Error\n ? error.message\n : \"Batch subscription failed\",\n });\n }\n }\n );\n }\n\n /**\n * Register unsubscription listener for a service.\n */\n private registerUnsubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance\n ): void {\n socket.on(\n eventName,\n (\n payload: { entryId: string },\n callback?: (\n response: ServiceResponse<{ unsubscribed: true; entryId: string }>\n ) => void\n ) => {\n try {\n service.unsubscribe(payload.entryId, socket);\n callback?.({\n success: true,\n data: { unsubscribed: true, entryId: payload.entryId },\n });\n } catch (error) {\n this.logger.error(`Error in unsubscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error:\n error instanceof Error ? error.message : \"Unsubscription failed\",\n });\n }\n }\n );\n }\n\n /**\n * Get list of registered service names.\n */\n public getServices(): string[] {\n return Array.from(this.services.keys());\n }\n\n /**\n * Get all service instances for lifecycle management.\n */\n public getServiceInstances(): BaseServiceInstance[] {\n return Array.from(this.services.values());\n }\n}\n","import express from \"express\";\nimport { createServer } from \"http\";\nimport { Server as SocketIOServer } from \"socket.io\";\nimport { consoleLogger } from \"../shared/types\";\nimport type {\n QuickdrawSocket,\n QuickdrawServerOptions,\n QuickdrawServerResult,\n} from \"./types\";\nimport { ServiceRegistry } from \"./ServiceRegistry\";\n\n/**\n * Create a fully configured quickdraw server with one function call.\n *\n * @example\n * ```typescript\n * import { createQuickdrawServer } from '@quickdraw/core/server';\n * import { ChatService, UserService } from './services';\n *\n * const { io, httpServer, registry } = createQuickdrawServer({\n * port: 4000,\n * cors: { origin: 'http://localhost:3000' },\n * services: {\n * chatService: new ChatService(prisma),\n * userService: new UserService(prisma),\n * },\n * auth: {\n * authenticate: async (socket, auth) => {\n * // Verify JWT and return userId\n * const payload = await verifyJWT(auth.token);\n * return payload.userId;\n * },\n * },\n * });\n * ```\n */\nexport function createQuickdrawServer(\n options: QuickdrawServerOptions\n): QuickdrawServerResult {\n const logger = options.logger ?? consoleLogger;\n const serverLogger = logger.child({ service: \"QuickdrawServer\" });\n\n // Create Express app with basic middleware\n const app = express();\n app.use(express.json());\n\n // Health check endpoint\n app.get(\"/health\", (_req, res) => {\n res.json({ status: \"ok\", timestamp: new Date().toISOString() });\n });\n\n // Create HTTP server\n const httpServer = createServer(app);\n\n // Create Socket.io server\n const io = new SocketIOServer(httpServer, {\n cors: {\n origin: options.cors?.origin ?? \"*\",\n methods: options.cors?.methods ?? [\"GET\", \"POST\"],\n credentials: options.cors?.credentials ?? true,\n },\n });\n\n // Create service registry with method logging configuration\n const registry = new ServiceRegistry(io, { \n logger,\n methodLogging: options.methodLogging,\n });\n\n // Apply authentication middleware\n io.use(async (socket, next) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n\n try {\n if (options.auth?.authenticate) {\n const auth = socket.handshake.auth as Record<string, unknown>;\n const userId = await options.auth.authenticate(quickdrawSocket, auth);\n quickdrawSocket.userId = userId;\n\n // TODO: Load service access from user record\n // For now, default to empty service access\n quickdrawSocket.serviceAccess = {};\n }\n next();\n } catch (error) {\n serverLogger.error(\"Authentication failed\", {\n error: error instanceof Error ? error.message : \"Unknown error\",\n socketId: socket.id,\n });\n next(new Error(\"Authentication failed\"));\n }\n });\n\n // Register services\n for (const [serviceName, service] of Object.entries(options.services)) {\n registry.registerService(serviceName, service);\n }\n\n // Handle connection lifecycle\n io.on(\"connection\", (socket) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n serverLogger.info(\"Socket connected\", {\n socketId: quickdrawSocket.id,\n userId: quickdrawSocket.userId,\n });\n\n quickdrawSocket.on(\"disconnect\", () => {\n serverLogger.info(\"Socket disconnected\", {\n socketId: quickdrawSocket.id,\n userId: quickdrawSocket.userId,\n });\n\n // Cleanup subscriptions across all services\n for (const service of registry.getServiceInstances()) {\n try {\n service.unsubscribeSocket(quickdrawSocket);\n } catch {\n // Ignore cleanup errors\n }\n }\n });\n });\n\n // Start listening\n httpServer.listen(options.port, \"0.0.0.0\", () => {\n serverLogger.info(`Server listening on port ${options.port}`);\n serverLogger.info(\n `Registered services: ${registry.getServices().join(\", \")}`\n );\n });\n\n // Graceful shutdown handler\n const shutdown = async (signal: string) => {\n serverLogger.info(`Received ${signal}, starting graceful shutdown...`);\n\n // Stop accepting new connections\n httpServer.close(() => {\n serverLogger.info(\"HTTP server closed\");\n });\n\n // Close all Socket.io connections gracefully\n const sockets = await io.fetchSockets();\n serverLogger.info(`Closing ${sockets.length} active socket connections...`);\n\n for (const socket of sockets) {\n const quickdrawSocket = socket as unknown as QuickdrawSocket;\n \n // Unsubscribe from all services\n for (const service of registry.getServiceInstances()) {\n try {\n service.unsubscribeSocket(quickdrawSocket);\n } catch {\n // Ignore cleanup errors\n }\n }\n\n // Disconnect socket\n socket.disconnect(true);\n }\n\n // Close Socket.io server\n io.close(() => {\n serverLogger.info(\"Socket.io server closed\");\n process.exit(0);\n });\n\n // Force exit after timeout\n setTimeout(() => {\n serverLogger.error(\"Graceful shutdown timeout, forcing exit\");\n process.exit(1);\n }, 10000); // 10 second timeout\n };\n\n // Register shutdown handlers\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n\n return { io, httpServer, registry };\n}\n","import { io as ioClient, Socket } from \"socket.io-client\";\nimport type { ServiceResponse } from \"../shared/types\";\nimport type { BaseServiceInstance, QuickdrawServerResult } from \"./types\";\nimport { createQuickdrawServer } from \"./createServer\";\n\n/**\n * Options for creating a test server.\n */\nexport interface CreateTestServerOptions {\n services: Record<string, BaseServiceInstance>;\n seedDb?: () => Promise<void>;\n}\n\n/**\n * Test server instance with helper methods.\n */\nexport interface TestServer {\n port: number;\n result: QuickdrawServerResult;\n stop: () => Promise<void>;\n connectAs: (userId: string) => Promise<TestClient>;\n}\n\n/**\n * Test client instance with helper methods.\n */\nexport interface TestClient {\n socket: Socket;\n emit: <TPayload, TResponse>(\n event: string,\n payload: TPayload\n ) => Promise<TResponse>;\n close: () => void;\n}\n\nlet nextPort = 10000;\n\n/**\n * Get an available port for testing.\n */\nexport function getAvailablePort(): number {\n return nextPort++;\n}\n\n/**\n * Create a test server with dev authentication.\n *\n * @example\n * ```typescript\n * const server = await createTestServer({\n * services: {\n * chatService: new ChatService(testPrisma),\n * },\n * seedDb: async () => {\n * await testPrisma.user.create({ ... });\n * },\n * });\n *\n * const client = await server.connectAs('user-id-123');\n * const result = await client.emit('chatService:createChat', { title: 'Test' });\n *\n * await server.stop();\n * ```\n */\nexport async function createTestServer(\n options: CreateTestServerOptions\n): Promise<TestServer> {\n const port = getAvailablePort();\n\n const result = createQuickdrawServer({\n port,\n services: options.services,\n auth: {\n // Dev mode: accept userId directly from auth\n authenticate: async (_socket, auth) => {\n return auth.userId as string | undefined;\n },\n },\n });\n\n // Wait for server to start\n await new Promise<void>((resolve) => {\n result.httpServer.once(\"listening\", resolve);\n });\n\n // Seed database if provided\n if (options.seedDb) {\n await options.seedDb();\n }\n\n return {\n port,\n result,\n stop: () =>\n new Promise<void>((resolve) => {\n result.io.close();\n result.httpServer.close(() => resolve());\n }),\n connectAs: (userId: string) => connectAsUser(port, userId),\n };\n}\n\n/**\n * Connect to a test server as a specific user.\n */\nexport async function connectAsUser(\n port: number,\n userId: string\n): Promise<TestClient> {\n const socket = ioClient(`http://localhost:${port}`, {\n auth: { userId },\n transports: [\"websocket\"],\n autoConnect: true,\n });\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(\"Connection timeout\"));\n }, 5000);\n\n socket.on(\"connect\", () => {\n clearTimeout(timeout);\n resolve();\n });\n\n socket.on(\"connect_error\", (err) => {\n clearTimeout(timeout);\n reject(err);\n });\n });\n\n return {\n socket,\n emit: <TPayload, TResponse>(event: string, payload: TPayload) =>\n emitWithAck<TPayload, TResponse>(socket, event, payload),\n close: () => socket.close(),\n };\n}\n\n/**\n * Emit an event and wait for acknowledgment.\n */\nexport function emitWithAck<TPayload, TResponse>(\n socket: Socket,\n event: string,\n payload: TPayload,\n timeoutMs: number = 5000\n): Promise<TResponse> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(`Timeout waiting for ${event}`));\n }, timeoutMs);\n\n socket.emit(event, payload, (response: ServiceResponse<TResponse>) => {\n clearTimeout(timeout);\n\n if (response.success) {\n resolve(response.data);\n } else {\n reject(new Error(response.error));\n }\n });\n });\n}\n\n/**\n * Wait for a specific socket event.\n */\nexport function waitForEvent<T>(\n socket: Socket,\n event: string,\n timeoutMs: number = 5000\n): Promise<T> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n socket.off(event, handler);\n reject(new Error(`Timeout waiting for event ${event}`));\n }, timeoutMs);\n\n const handler = (data: T) => {\n clearTimeout(timeout);\n socket.off(event, handler);\n resolve(data);\n };\n\n socket.on(event, handler);\n });\n}\n"]}
1
+ {"version":3,"sources":["../../src/shared/types.ts","../../src/server/ServiceRegistry.ts","../../src/server/createServer.ts","../../src/server/testing.ts"],"names":["result","SocketIOServer","ioClient"],"mappings":"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","file":"testing.js","sourcesContent":["import type { z } from \"zod\";\n\n// ============================================================================\n// Access Control Types\n// ============================================================================\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n\n// ============================================================================\n// Entity and User Interfaces for ACL\n// ============================================================================\n\n/**\n * Interface for entities that support entry-level ACL stored as JSON.\n *\n * Use this pattern when:\n * - You want simple ACL without a separate membership table\n * - Access patterns are straightforward (read/write permissions)\n * - You don't need to query \"all entities user X can access\" efficiently\n *\n * The default `checkEntryACL` in BaseService expects entities to have this shape.\n *\n * @example\n * ```prisma\n * model Document {\n * id String @id @default(cuid())\n * title String\n * acl Json? // Stores ACL array\n * }\n * ```\n *\n * @example\n * ```typescript\n * class DocumentService extends BaseService<Document & ACLEntity, ...> {\n * constructor(prisma: PrismaClient) {\n * super({ serviceName: 'documentService', hasEntryACL: true });\n * this.setDelegate(prisma.document);\n * // Default checkEntryACL will work automatically\n * }\n * }\n * ```\n */\nexport interface ACLEntity {\n id: string;\n acl?: ACL | null;\n}\n\n/**\n * Interface for users with service-level access control.\n *\n * The `serviceAccess` field maps service names to access levels, granting\n * blanket permissions across all entries in that service.\n *\n * @example\n * ```prisma\n * model User {\n * id String @id @default(cuid())\n * serviceAccess Json? // { \"chatService\": \"Admin\", \"documentService\": \"Read\" }\n * }\n * ```\n *\n * @example\n * ```typescript\n * // User with Admin access to chatService can perform any operation\n * // on any chat, regardless of entry-level ACL\n * const user: QuickdrawUser = {\n * id: \"user-123\",\n * serviceAccess: { chatService: \"Admin\" }\n * };\n * ```\n */\nexport interface QuickdrawUser {\n id: string;\n serviceAccess?: Record<string, AccessLevel> | null;\n}\n\n// ============================================================================\n// Service Response Types\n// ============================================================================\n\nexport type ServiceResponse<T = unknown> =\n | { success: true; data: T }\n | { success: false; error: string; code?: number };\n\n// ============================================================================\n// Service Method Definition Types\n// ============================================================================\n\n/**\n * Defines the shape of a service method including its payload and response types.\n * Used to create a type-safe contract between server and client.\n */\nexport interface ServiceMethodDefinition<TPayload = unknown, TResponse = unknown> {\n name: string;\n access: AccessLevel;\n handler: (payload: TPayload, context: ServiceMethodContext) => Promise<TResponse>;\n schema?: z.ZodType<TPayload>;\n resolveEntryId?: (payload: TPayload) => string | null;\n}\n\n/**\n * Context provided to service method handlers\n */\nexport interface ServiceMethodContext {\n userId: string | undefined;\n socketId: string;\n serviceAccess: Record<string, AccessLevel>;\n}\n\n/**\n * Type helper for defining service method maps.\n * Each service should define its methods using this structure.\n *\n * @example\n * ```typescript\n * type ChatServiceMethods = ServiceMethodMap<{\n * createChat: {\n * payload: { title: string };\n * response: { id: string };\n * };\n * updateTitle: {\n * payload: { id: string; title: string };\n * response: { id: string; title: string };\n * };\n * }>;\n * ```\n */\nexport type ServiceMethodMap<T extends Record<string, { payload: unknown; response: unknown }>> = T;\n\n// ============================================================================\n// Admin Method Types (Generic shapes for all services)\n// ============================================================================\n\nexport type AdminListPayload = {\n page?: number;\n pageSize?: number;\n sort?: {\n field?: string;\n direction?: \"asc\" | \"desc\";\n };\n filter?: {\n id?: string;\n ids?: string[];\n createdAfter?: string;\n createdBefore?: string;\n updatedAfter?: string;\n updatedBefore?: string;\n } & Record<string, unknown>;\n};\n\nexport type AdminListResponse<T> = {\n rows: T[];\n page: number;\n pageSize: number;\n total: number;\n};\n\nexport type AdminGetPayload = { id: string };\n\nexport type AdminCreatePayload<TInsert> = { data: Partial<TInsert> };\n\nexport type AdminUpdatePayload<TInsert> = {\n id: string;\n data: Partial<TInsert>;\n};\n\nexport type AdminDeletePayload = { id: string };\nexport type AdminDeleteResponse = { id: string; deleted: true };\n\nexport type AdminSetEntryACLPayload = {\n id: string;\n acl: ACL;\n};\n\nexport type AdminGetSubscribersPayload = { id: string };\nexport type AdminGetSubscribersResponse = {\n id: string;\n subscribers: Array<{ socketId: string; userId?: string }>;\n};\n\nexport type AdminReemitPayload = { id: string };\nexport type AdminReemitResponse = { emitted: boolean };\n\nexport type AdminUnsubscribeAllPayload = { id: string };\nexport type AdminUnsubscribeAllResponse = { id: string; unsubscribed: number };\n\n// ============================================================================\n// Admin Field Configuration Types\n// ============================================================================\n\n/**\n * Supported field types for admin UI generation.\n */\nexport type AdminFieldType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"enum\"\n | \"json\"\n | \"relation\";\n\n/**\n * Configuration for a single field in the admin UI.\n * Derived from Zod schemas with optional overrides.\n */\nexport interface AdminFieldConfig {\n /** Field name (property key on the entity) */\n name: string;\n /** Field type for rendering appropriate input */\n type: AdminFieldType;\n /** Human-readable label */\n label: string;\n /** Whether the field is required for creation */\n required: boolean;\n /** Whether the field can be edited (false for id, createdAt, etc.) */\n editable: boolean;\n /** Whether to show this field as a table column */\n showInTable: boolean;\n /** Whether the table can be sorted by this field */\n sortable: boolean;\n /** For enum fields, the allowed values */\n enumValues?: string[];\n /** For relation fields, the related service name */\n relationService?: string;\n}\n\n/**\n * Service metadata for admin UI generation.\n * Returned by the adminMeta method.\n */\nexport interface AdminServiceMeta {\n /** Internal service name (e.g., \"chatService\") */\n serviceName: string;\n /** Human-readable display name (e.g., \"Chats\") */\n displayName: string;\n /** Field configurations derived from schema */\n fields: AdminFieldConfig[];\n}\n\n/**\n * Payload for adminMeta method (no parameters needed)\n */\nexport type AdminMetaPayload = Record<string, never>;\n\n/**\n * Response type for adminMeta method\n */\nexport type AdminMetaResponse = AdminServiceMeta;\n\n// ============================================================================\n// Subscription Types\n// ============================================================================\n\nexport type SubscribePayload = {\n entryId: string;\n requiredLevel?: AccessLevel;\n};\n\nexport type UnsubscribePayload = {\n entryId: string;\n};\n\n// ============================================================================\n// Utility Types\n// ============================================================================\n\n/**\n * Extracts payload type from a service method definition\n */\nexport type ExtractPayload<T> = T extends { payload: infer P } ? P : never;\n\n/**\n * Extracts response type from a service method definition\n */\nexport type ExtractResponse<T> = T extends { response: infer R } ? R : never;\n\n/**\n * Logger interface compatible with Winston and other loggers\n */\nexport interface Logger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n debug(message: string, meta?: Record<string, unknown>): void;\n child(options: Record<string, unknown>): Logger;\n}\n\n/**\n * Default console logger implementation\n */\nexport const consoleLogger: Logger = {\n info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? \"\"),\n warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[ERROR] ${message}`, meta ?? \"\"),\n debug: (message, meta) => console.debug(`[DEBUG] ${message}`, meta ?? \"\"),\n child: () => consoleLogger,\n};\n","import type { Server as SocketIOServer } from \"socket.io\";\nimport type { ServiceResponse, ServiceMethodDefinition, Logger } from \"../shared/types\";\nimport { consoleLogger } from \"../shared/types\";\nimport type { QuickdrawSocket, BaseServiceInstance, ServiceRegistryInstance } from \"./types\";\n\n/**\n * Central registry for auto-discovering and registering service methods as Socket.io events.\n *\n * Features:\n * - Auto-discovers public methods from registered services\n * - Wires methods to Socket.io events with standardized naming\n * - Handles subscription lifecycle\n * - Provides consistent error handling and logging\n * - Uses a single connection handler for all services (memory efficient)\n *\n * @example\n * ```typescript\n * const registry = new ServiceRegistry(io, { logger });\n * registry.registerService('chatService', chatService);\n * registry.registerService('userService', userService);\n * ```\n */\nexport interface ServiceRegistryOptions {\n logger?: Logger;\n /**\n * Automatic method logging configuration.\n * @default { enabled: true, logPayloads: false, logResponses: false }\n */\n methodLogging?: {\n enabled?: boolean;\n logPayloads?: boolean;\n logResponses?: boolean;\n };\n}\n\nexport class ServiceRegistry implements ServiceRegistryInstance {\n private readonly io: SocketIOServer;\n private readonly services: Map<string, BaseServiceInstance> = new Map();\n private readonly logger: Logger;\n private readonly methodLoggingEnabled: boolean;\n private readonly logPayloads: boolean;\n private readonly logResponses: boolean;\n private connectionHandlerInstalled = false;\n\n // Store method metadata for deferred registration\n private readonly methodRegistry: Map<\n string,\n {\n serviceName: string;\n method: ServiceMethodDefinition<unknown, unknown>;\n service: BaseServiceInstance;\n }\n > = new Map();\n\n constructor(io: SocketIOServer, options?: ServiceRegistryOptions) {\n this.io = io;\n this.logger =\n options?.logger?.child({ service: \"ServiceRegistry\" }) ??\n consoleLogger.child({ service: \"ServiceRegistry\" });\n\n // Method logging configuration (enabled by default)\n this.methodLoggingEnabled = options?.methodLogging?.enabled ?? true;\n this.logPayloads = options?.methodLogging?.logPayloads ?? false;\n this.logResponses = options?.methodLogging?.logResponses ?? false;\n\n if (this.methodLoggingEnabled) {\n this.logger.info(\"Automatic method logging enabled\", {\n logPayloads: this.logPayloads,\n logResponses: this.logResponses,\n });\n }\n }\n\n /**\n * Install the single connection handler that wires all registered services.\n * This is called lazily on first service registration.\n */\n private installConnectionHandler(): void {\n if (this.connectionHandlerInstalled) return;\n this.connectionHandlerInstalled = true;\n\n this.io.on(\"connection\", (socket) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n this.setupSocketListeners(quickdrawSocket);\n });\n\n this.logger.info(\"Installed single connection handler for all services\");\n }\n\n /**\n * Set up all event listeners for a newly connected socket.\n */\n private setupSocketListeners(socket: QuickdrawSocket): void {\n // Register method listeners for all registered services\n for (const [eventName, { method, service }] of this.methodRegistry) {\n this.registerMethodListener(socket, eventName, method, service);\n }\n\n // Register subscription/unsubscription listeners for all services\n for (const [serviceName, service] of this.services) {\n this.registerSubscriptionListener(socket, `${serviceName}:subscribe`, service);\n this.registerBatchSubscriptionListener(socket, `${serviceName}:batchSubscribe`, service);\n this.registerUnsubscriptionListener(socket, `${serviceName}:unsubscribe`, service);\n }\n }\n\n /**\n * Register a service and auto-discover its public methods.\n */\n public registerService(serviceName: string, service: BaseServiceInstance): void {\n this.services.set(serviceName, service);\n\n // Pass io instance to service for room-based broadcasts\n if (service.setIo) {\n service.setIo(this.io);\n }\n\n this.logger.info(`Registered service: ${serviceName}`);\n\n // Auto-discover and store method metadata\n this.discoverServiceMethods(serviceName, service);\n\n // Ensure connection handler is installed\n this.installConnectionHandler();\n }\n\n /**\n * Discover and store all public methods from a service.\n */\n private discoverServiceMethods(serviceName: string, service: BaseServiceInstance): void {\n const methods = service.getPublicMethods();\n\n for (const method of methods) {\n const eventName = `${serviceName}:${method.name}`;\n this.logger.info(`Registering socket event: ${eventName}`);\n\n // Store method metadata for deferred registration on connection\n this.methodRegistry.set(eventName, {\n serviceName,\n method,\n service,\n });\n }\n }\n\n /**\n * Register a method as a socket event listener.\n */\n private registerMethodListener(\n socket: QuickdrawSocket,\n eventName: string,\n method: ServiceMethodDefinition<unknown, unknown>,\n service: BaseServiceInstance,\n ): void {\n socket.on(\n eventName,\n async (payload: unknown, callback?: (response: ServiceResponse<unknown>) => void) => {\n const startedAt = Date.now();\n const [serviceName, methodName] = eventName.split(\":\");\n const userIdShort = socket.userId?.slice(0, 8) ?? \"anon\";\n\n try {\n // Authentication check\n if (!socket.userId && method.access !== \"Public\") {\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: \"Authentication required\",\n code: 401,\n };\n callback?.(errorResponse);\n return;\n }\n\n // Validate payload with Zod schema if provided\n let validatedPayload = payload;\n if (method.schema) {\n const result = method.schema.safeParse(payload);\n if (!result.success) {\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: `Validation error: ${result.error.message}`,\n code: 400,\n };\n callback?.(errorResponse);\n return;\n }\n validatedPayload = result.data;\n }\n\n // Resolve entry ID for access check\n let entryId: string | undefined;\n if (method.resolveEntryId) {\n entryId = method.resolveEntryId(validatedPayload) ?? undefined;\n } else if (\n validatedPayload &&\n typeof validatedPayload === \"object\" &&\n \"id\" in validatedPayload\n ) {\n const idValue = (validatedPayload as Record<string, unknown>).id;\n if (typeof idValue === \"string\") {\n entryId = idValue;\n }\n }\n\n // Check access\n await service.ensureAccessForMethod(method.access, socket, entryId);\n\n // Log start (if enabled)\n if (this.methodLoggingEnabled) {\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n serviceName,\n methodName,\n userId: socket.userId,\n socketId: socket.id,\n };\n if (this.logPayloads) {\n logContext.payload = validatedPayload;\n }\n this.logger.info(\n `User ${userIdShort} ${serviceName}.${methodName} -> start`,\n logContext,\n );\n }\n\n // Execute handler\n const result = await method.handler(validatedPayload, {\n userId: socket.userId,\n socketId: socket.id,\n serviceAccess: socket.serviceAccess ?? {},\n });\n\n // Log success (if enabled)\n if (this.methodLoggingEnabled) {\n const durationMs = Date.now() - startedAt;\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n outcome: \"success\",\n durationMs,\n serviceName,\n methodName,\n userId: socket.userId,\n };\n if (this.logResponses) {\n logContext.response = result;\n }\n this.logger.info(\n `User ${userIdShort} ${serviceName}.${methodName} -> success`,\n logContext,\n );\n }\n\n const successResponse: ServiceResponse<unknown> = {\n success: true,\n data: result,\n };\n callback?.(successResponse);\n } catch (error) {\n // Always log errors, even if methodLogging is disabled\n const durationMs = Date.now() - startedAt;\n const logContext: Record<string, unknown> = {\n category: \"request_processing\",\n outcome: \"fail\",\n durationMs,\n serviceName,\n methodName,\n userId: socket.userId,\n socketId: socket.id,\n error: error instanceof Error ? error.message : \"Unknown error\",\n };\n if (this.logPayloads) {\n logContext.payload = payload;\n }\n this.logger.error(`User ${userIdShort} ${serviceName}.${methodName} -> fail`, logContext);\n\n const errorResponse: ServiceResponse<unknown> = {\n success: false,\n error: error instanceof Error ? error.message : \"Internal error\",\n code: 500,\n };\n callback?.(errorResponse);\n }\n },\n );\n }\n\n /**\n * Register subscription listener for a service.\n */\n private registerSubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance,\n ): void {\n socket.on(\n eventName,\n async (\n payload: { entryId: string; requiredLevel?: string },\n callback?: (response: ServiceResponse<unknown>) => void,\n ) => {\n try {\n if (!socket.userId) {\n callback?.({\n success: false,\n error: \"Authentication required\",\n code: 401,\n });\n return;\n }\n\n const data = await service.subscribe(\n payload.entryId,\n socket,\n (payload.requiredLevel as \"Read\" | \"Moderate\" | \"Admin\") ?? \"Read\",\n );\n\n if (data === null) {\n callback?.({\n success: false,\n error: \"Access denied or entry not found\",\n code: 403,\n });\n return;\n }\n\n callback?.({ success: true, data });\n } catch (error) {\n this.logger.error(`Error in subscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error: error instanceof Error ? error.message : \"Subscription failed\",\n });\n }\n },\n );\n }\n\n /**\n * Register batch subscription listener for a service.\n * Handles subscribing to multiple entities in a single event.\n */\n private registerBatchSubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance,\n ): void {\n socket.on(\n eventName,\n async (\n payload: { entryIds: string[]; requiredLevel?: string },\n callback?: (response: ServiceResponse<Record<string, unknown>>) => void,\n ) => {\n try {\n if (!socket.userId) {\n callback?.({\n success: false,\n error: \"Authentication required\",\n code: 401,\n });\n return;\n }\n\n if (!Array.isArray(payload.entryIds) || payload.entryIds.length === 0) {\n callback?.({\n success: false,\n error: \"entryIds must be a non-empty array\",\n code: 400,\n });\n return;\n }\n\n const data = await service.batchSubscribe(\n payload.entryIds,\n socket,\n (payload.requiredLevel as \"Read\" | \"Moderate\" | \"Admin\") ?? \"Read\",\n );\n\n callback?.({ success: true, data });\n } catch (error) {\n this.logger.error(`Error in batch subscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error: error instanceof Error ? error.message : \"Batch subscription failed\",\n });\n }\n },\n );\n }\n\n /**\n * Register unsubscription listener for a service.\n */\n private registerUnsubscriptionListener(\n socket: QuickdrawSocket,\n eventName: string,\n service: BaseServiceInstance,\n ): void {\n socket.on(\n eventName,\n (\n payload: { entryId: string },\n callback?: (response: ServiceResponse<{ unsubscribed: true; entryId: string }>) => void,\n ) => {\n try {\n service.unsubscribe(payload.entryId, socket);\n callback?.({\n success: true,\n data: { unsubscribed: true, entryId: payload.entryId },\n });\n } catch (error) {\n this.logger.error(`Error in unsubscription ${eventName}:`, {\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n callback?.({\n success: false,\n error: error instanceof Error ? error.message : \"Unsubscription failed\",\n });\n }\n },\n );\n }\n\n /**\n * Get list of registered service names.\n */\n public getServices(): string[] {\n return Array.from(this.services.keys());\n }\n\n /**\n * Get all service instances for lifecycle management.\n */\n public getServiceInstances(): BaseServiceInstance[] {\n return Array.from(this.services.values());\n }\n}\n","import express from \"express\";\nimport { createServer } from \"http\";\nimport { Server as SocketIOServer } from \"socket.io\";\nimport { consoleLogger } from \"../shared/types\";\nimport type { QuickdrawSocket, QuickdrawServerOptions, QuickdrawServerResult } from \"./types\";\nimport { ServiceRegistry } from \"./ServiceRegistry\";\n\n/**\n * Create a fully configured quickdraw server with one function call.\n *\n * @example\n * ```typescript\n * import { createQuickdrawServer } from '@quickdraw/core/server';\n * import { ChatService, UserService } from './services';\n *\n * const { io, httpServer, registry } = createQuickdrawServer({\n * port: 4000,\n * cors: { origin: 'http://localhost:3000' },\n * services: {\n * chatService: new ChatService(prisma),\n * userService: new UserService(prisma),\n * },\n * auth: {\n * authenticate: async (socket, auth) => {\n * // Verify JWT and return userId\n * const payload = await verifyJWT(auth.token);\n * return payload.userId;\n * },\n * },\n * });\n * ```\n */\nexport function createQuickdrawServer(options: QuickdrawServerOptions): QuickdrawServerResult {\n const logger = options.logger ?? consoleLogger;\n const serverLogger = logger.child({ service: \"QuickdrawServer\" });\n\n // Create Express app with basic middleware\n const app = express();\n app.use(express.json());\n\n // Health check endpoint\n app.get(\"/health\", (_req, res) => {\n res.json({ status: \"ok\", timestamp: new Date().toISOString() });\n });\n\n // Create HTTP server\n const httpServer = createServer(app);\n\n // Create Socket.io server\n const io = new SocketIOServer(httpServer, {\n cors: {\n origin: options.cors?.origin ?? \"*\",\n methods: options.cors?.methods ?? [\"GET\", \"POST\"],\n credentials: options.cors?.credentials ?? true,\n },\n });\n\n // Create service registry with method logging configuration\n const registry = new ServiceRegistry(io, {\n logger,\n methodLogging: options.methodLogging,\n });\n\n // Apply authentication middleware\n io.use(async (socket, next) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n\n try {\n if (options.auth?.authenticate) {\n const auth = socket.handshake.auth as Record<string, unknown>;\n const userId = await options.auth.authenticate(quickdrawSocket, auth);\n quickdrawSocket.userId = userId;\n\n // TODO: Load service access from user record\n // For now, default to empty service access\n quickdrawSocket.serviceAccess = {};\n }\n next();\n } catch (error) {\n serverLogger.error(\"Authentication failed\", {\n error: error instanceof Error ? error.message : \"Unknown error\",\n socketId: socket.id,\n });\n next(new Error(\"Authentication failed\"));\n }\n });\n\n // Register services\n for (const [serviceName, service] of Object.entries(options.services)) {\n registry.registerService(serviceName, service);\n }\n\n // Handle connection lifecycle\n io.on(\"connection\", (socket) => {\n const quickdrawSocket = socket as QuickdrawSocket;\n serverLogger.info(\"Socket connected\", {\n socketId: quickdrawSocket.id,\n userId: quickdrawSocket.userId,\n });\n\n quickdrawSocket.on(\"disconnect\", () => {\n serverLogger.info(\"Socket disconnected\", {\n socketId: quickdrawSocket.id,\n userId: quickdrawSocket.userId,\n });\n\n // Cleanup subscriptions across all services\n for (const service of registry.getServiceInstances()) {\n try {\n service.unsubscribeSocket(quickdrawSocket);\n } catch {\n // Ignore cleanup errors\n }\n }\n });\n });\n\n // Start listening\n httpServer.listen(options.port, \"0.0.0.0\", () => {\n serverLogger.info(`Server listening on port ${options.port}`);\n serverLogger.info(`Registered services: ${registry.getServices().join(\", \")}`);\n });\n\n // Graceful shutdown handler\n const shutdown = async (signal: string) => {\n serverLogger.info(`Received ${signal}, starting graceful shutdown...`);\n\n // Stop accepting new connections\n httpServer.close(() => {\n serverLogger.info(\"HTTP server closed\");\n });\n\n // Close all Socket.io connections gracefully\n const sockets = await io.fetchSockets();\n serverLogger.info(`Closing ${sockets.length} active socket connections...`);\n\n for (const socket of sockets) {\n const quickdrawSocket = socket as unknown as QuickdrawSocket;\n\n // Unsubscribe from all services\n for (const service of registry.getServiceInstances()) {\n try {\n service.unsubscribeSocket(quickdrawSocket);\n } catch {\n // Ignore cleanup errors\n }\n }\n\n // Disconnect socket\n socket.disconnect(true);\n }\n\n // Close Socket.io server\n io.close(() => {\n serverLogger.info(\"Socket.io server closed\");\n process.exit(0);\n });\n\n // Force exit after timeout\n setTimeout(() => {\n serverLogger.error(\"Graceful shutdown timeout, forcing exit\");\n process.exit(1);\n }, 10000); // 10 second timeout\n };\n\n // Register shutdown handlers\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n\n return { io, httpServer, registry };\n}\n","import { io as ioClient, Socket } from \"socket.io-client\";\nimport type { ServiceResponse } from \"../shared/types\";\nimport type { BaseServiceInstance, QuickdrawServerResult } from \"./types\";\nimport { createQuickdrawServer } from \"./createServer\";\n\n/**\n * Options for creating a test server.\n */\nexport interface CreateTestServerOptions {\n services: Record<string, BaseServiceInstance>;\n seedDb?: () => Promise<void>;\n}\n\n/**\n * Test server instance with helper methods.\n */\nexport interface TestServer {\n port: number;\n result: QuickdrawServerResult;\n stop: () => Promise<void>;\n connectAs: (userId: string) => Promise<TestClient>;\n}\n\n/**\n * Test client instance with helper methods.\n */\nexport interface TestClient {\n socket: Socket;\n emit: <TPayload, TResponse>(event: string, payload: TPayload) => Promise<TResponse>;\n close: () => void;\n}\n\nlet nextPort = 10000;\n\n/**\n * Get an available port for testing.\n */\nexport function getAvailablePort(): number {\n return nextPort++;\n}\n\n/**\n * Create a test server with dev authentication.\n *\n * @example\n * ```typescript\n * const server = await createTestServer({\n * services: {\n * chatService: new ChatService(testPrisma),\n * },\n * seedDb: async () => {\n * await testPrisma.user.create({ ... });\n * },\n * });\n *\n * const client = await server.connectAs('user-id-123');\n * const result = await client.emit('chatService:createChat', { title: 'Test' });\n *\n * await server.stop();\n * ```\n */\nexport async function createTestServer(options: CreateTestServerOptions): Promise<TestServer> {\n const port = getAvailablePort();\n\n const result = createQuickdrawServer({\n port,\n services: options.services,\n auth: {\n // Dev mode: accept userId directly from auth\n authenticate: async (_socket, auth) => {\n return auth.userId as string | undefined;\n },\n },\n });\n\n // Wait for server to start\n await new Promise<void>((resolve) => {\n result.httpServer.once(\"listening\", resolve);\n });\n\n // Seed database if provided\n if (options.seedDb) {\n await options.seedDb();\n }\n\n return {\n port,\n result,\n stop: () =>\n new Promise<void>((resolve) => {\n result.io.close();\n result.httpServer.close(() => resolve());\n }),\n connectAs: (userId: string) => connectAsUser(port, userId),\n };\n}\n\n/**\n * Connect to a test server as a specific user.\n */\nexport async function connectAsUser(port: number, userId: string): Promise<TestClient> {\n const socket = ioClient(`http://localhost:${port}`, {\n auth: { userId },\n transports: [\"websocket\"],\n autoConnect: true,\n });\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(\"Connection timeout\"));\n }, 5000);\n\n socket.on(\"connect\", () => {\n clearTimeout(timeout);\n resolve();\n });\n\n socket.on(\"connect_error\", (err) => {\n clearTimeout(timeout);\n reject(err);\n });\n });\n\n return {\n socket,\n emit: <TPayload, TResponse>(event: string, payload: TPayload) =>\n emitWithAck<TPayload, TResponse>(socket, event, payload),\n close: () => socket.close(),\n };\n}\n\n/**\n * Emit an event and wait for acknowledgment.\n */\nexport function emitWithAck<TPayload, TResponse>(\n socket: Socket,\n event: string,\n payload: TPayload,\n timeoutMs: number = 5000,\n): Promise<TResponse> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(`Timeout waiting for ${event}`));\n }, timeoutMs);\n\n socket.emit(event, payload, (response: ServiceResponse<TResponse>) => {\n clearTimeout(timeout);\n\n if (response.success) {\n resolve(response.data);\n } else {\n reject(new Error(response.error));\n }\n });\n });\n}\n\n/**\n * Wait for a specific socket event.\n */\nexport function waitForEvent<T>(\n socket: Socket,\n event: string,\n timeoutMs: number = 5000,\n): Promise<T> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n socket.off(event, handler);\n reject(new Error(`Timeout waiting for event ${event}`));\n }, timeoutMs);\n\n const handler = (data: T) => {\n clearTimeout(timeout);\n socket.off(event, handler);\n resolve(data);\n };\n\n socket.on(event, handler);\n });\n}\n"]}