@fitzzero/quickdraw-core 1.0.0 → 1.2.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/client/index.d.ts +49 -3
- package/dist/client/index.js +157 -33
- package/dist/client/index.js.map +1 -1
- package/dist/client/testing.d.ts +1 -1
- package/dist/server/index.d.ts +162 -6
- package/dist/server/index.js +346 -20
- package/dist/server/index.js.map +1 -1
- package/dist/server/testing.d.ts +2 -2
- package/dist/server/testing.js +79 -16
- package/dist/server/testing.js.map +1 -1
- package/dist/shared/index.d.ts +49 -1
- package/dist/shared/index.js.map +1 -1
- package/dist/{types-CDhCEpTU.d.ts → types-D7oHWh4O.d.ts} +29 -1
- package/dist/{types-BADirRXn.d.ts → types-e6WPGE70.d.ts} +91 -1
- package/package.json +13 -12
package/dist/server/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as QuickdrawSocket, P as PrismaDelegate, b as BaseServiceOptions, I as InstallAdminMethodsOptions, A as AdminListPayload, c as AdminListResponse, d as AdminSetACLPayload, e as AdminSubscribersResponse, S as ServiceRegistryInstance, B as BaseServiceInstance, f as QuickdrawServerOptions, Q as QuickdrawServerResult } from '../types-
|
|
1
|
+
import { a as QuickdrawSocket, P as PrismaDelegate, b as BaseServiceOptions, I as InstallAdminMethodsOptions, A as AdminListPayload, c as AdminListResponse, d as AdminSetACLPayload, e as AdminSubscribersResponse, S as ServiceRegistryInstance, B as BaseServiceInstance, f as QuickdrawServerOptions, Q as QuickdrawServerResult } from '../types-D7oHWh4O.js';
|
|
2
2
|
import { Server, Socket } from 'socket.io';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { ACL, Logger, AccessLevel, ServiceMethodContext, ServiceMethodDefinition } from '../shared/index.js';
|
|
4
|
+
import { ACL, Logger, AccessLevel, ServiceMethodContext, ServiceMethodDefinition, AdminServiceMeta, AdminFieldConfig } from '../shared/index.js';
|
|
5
5
|
import 'http';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -54,6 +54,7 @@ declare abstract class BaseService<TEntity extends {
|
|
|
54
54
|
protected delegate: PrismaDelegate<TEntity, TCreateInput, TUpdateInput> | undefined;
|
|
55
55
|
protected io: Server | null;
|
|
56
56
|
private readonly publicMethods;
|
|
57
|
+
private adminMeta;
|
|
57
58
|
constructor(options: BaseServiceOptions);
|
|
58
59
|
/**
|
|
59
60
|
* Set the Prisma delegate for this service.
|
|
@@ -242,10 +243,22 @@ declare abstract class BaseService<TEntity extends {
|
|
|
242
243
|
* reemit: "Admin",
|
|
243
244
|
* unsubscribeAll: "Admin",
|
|
244
245
|
* },
|
|
246
|
+
* schema: createEntitySchema,
|
|
247
|
+
* displayName: "Entities",
|
|
245
248
|
* });
|
|
246
249
|
* ```
|
|
247
250
|
*/
|
|
248
251
|
protected installAdminMethods(options: InstallAdminMethodsOptions): void;
|
|
252
|
+
/**
|
|
253
|
+
* Convert service name to display name.
|
|
254
|
+
* e.g., "chatService" -> "Chats", "userService" -> "Users"
|
|
255
|
+
*/
|
|
256
|
+
private toDisplayName;
|
|
257
|
+
/**
|
|
258
|
+
* Get admin metadata for this service.
|
|
259
|
+
* Returns the configured metadata or throws if not configured.
|
|
260
|
+
*/
|
|
261
|
+
getAdminMeta(): AdminServiceMeta;
|
|
249
262
|
/**
|
|
250
263
|
* Admin method: List entities with pagination and filters.
|
|
251
264
|
*/
|
|
@@ -315,15 +328,28 @@ declare abstract class BaseService<TEntity extends {
|
|
|
315
328
|
* registry.registerService('userService', userService);
|
|
316
329
|
* ```
|
|
317
330
|
*/
|
|
331
|
+
interface ServiceRegistryOptions {
|
|
332
|
+
logger?: Logger;
|
|
333
|
+
/**
|
|
334
|
+
* Automatic method logging configuration.
|
|
335
|
+
* @default { enabled: true, logPayloads: false, logResponses: false }
|
|
336
|
+
*/
|
|
337
|
+
methodLogging?: {
|
|
338
|
+
enabled?: boolean;
|
|
339
|
+
logPayloads?: boolean;
|
|
340
|
+
logResponses?: boolean;
|
|
341
|
+
};
|
|
342
|
+
}
|
|
318
343
|
declare class ServiceRegistry implements ServiceRegistryInstance {
|
|
319
344
|
private readonly io;
|
|
320
345
|
private readonly services;
|
|
321
346
|
private readonly logger;
|
|
347
|
+
private readonly methodLoggingEnabled;
|
|
348
|
+
private readonly logPayloads;
|
|
349
|
+
private readonly logResponses;
|
|
322
350
|
private connectionHandlerInstalled;
|
|
323
351
|
private readonly methodRegistry;
|
|
324
|
-
constructor(io: Server, options?:
|
|
325
|
-
logger?: Logger;
|
|
326
|
-
});
|
|
352
|
+
constructor(io: Server, options?: ServiceRegistryOptions);
|
|
327
353
|
/**
|
|
328
354
|
* Install the single connection handler that wires all registered services.
|
|
329
355
|
* This is called lazily on first service registration.
|
|
@@ -745,4 +771,134 @@ interface GoogleUser {
|
|
|
745
771
|
*/
|
|
746
772
|
declare const googleProvider: OAuthProvider<GoogleUser>;
|
|
747
773
|
|
|
748
|
-
|
|
774
|
+
/**
|
|
775
|
+
* Environment variable validation utilities.
|
|
776
|
+
* Helps ensure required environment variables are set before the application starts.
|
|
777
|
+
*/
|
|
778
|
+
interface ValidateEnvOptions {
|
|
779
|
+
/**
|
|
780
|
+
* List of required environment variable names.
|
|
781
|
+
* Validation will fail if any of these are missing or empty.
|
|
782
|
+
*/
|
|
783
|
+
required: string[];
|
|
784
|
+
/**
|
|
785
|
+
* Only enforce validation in production environment.
|
|
786
|
+
* @default true
|
|
787
|
+
*/
|
|
788
|
+
productionOnly?: boolean;
|
|
789
|
+
/**
|
|
790
|
+
* Custom error message prefix.
|
|
791
|
+
* @default "Missing required environment variables"
|
|
792
|
+
*/
|
|
793
|
+
errorPrefix?: string;
|
|
794
|
+
}
|
|
795
|
+
interface EnvValidationResult {
|
|
796
|
+
success: boolean;
|
|
797
|
+
missing: string[];
|
|
798
|
+
message?: string;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Validate that required environment variables are set.
|
|
802
|
+
* Throws an error if any required variables are missing.
|
|
803
|
+
*
|
|
804
|
+
* @param options - Validation configuration
|
|
805
|
+
* @throws {Error} If required environment variables are missing
|
|
806
|
+
*
|
|
807
|
+
* @example
|
|
808
|
+
* ```typescript
|
|
809
|
+
* // Validate required vars on startup
|
|
810
|
+
* validateEnv({
|
|
811
|
+
* required: ['JWT_SECRET', 'DATABASE_URL', 'CLIENT_URL'],
|
|
812
|
+
* });
|
|
813
|
+
* ```
|
|
814
|
+
*
|
|
815
|
+
* @example
|
|
816
|
+
* ```typescript
|
|
817
|
+
* // Only validate in production
|
|
818
|
+
* validateEnv({
|
|
819
|
+
* required: ['JWT_SECRET', 'DATABASE_URL'],
|
|
820
|
+
* productionOnly: true,
|
|
821
|
+
* });
|
|
822
|
+
* ```
|
|
823
|
+
*/
|
|
824
|
+
declare function validateEnv(options: ValidateEnvOptions): void;
|
|
825
|
+
/**
|
|
826
|
+
* Check environment variables without throwing.
|
|
827
|
+
* Returns a result object with validation status and missing variables.
|
|
828
|
+
*
|
|
829
|
+
* @param options - Validation configuration
|
|
830
|
+
* @returns Validation result with missing variables list
|
|
831
|
+
*
|
|
832
|
+
* @example
|
|
833
|
+
* ```typescript
|
|
834
|
+
* const result = checkEnv({ required: ['JWT_SECRET'] });
|
|
835
|
+
* if (!result.success) {
|
|
836
|
+
* console.warn('Missing env vars:', result.missing);
|
|
837
|
+
* }
|
|
838
|
+
* ```
|
|
839
|
+
*/
|
|
840
|
+
declare function checkEnv(options: ValidateEnvOptions): EnvValidationResult;
|
|
841
|
+
/**
|
|
842
|
+
* Get an environment variable or throw if it's missing.
|
|
843
|
+
* Useful for individual variable access with validation.
|
|
844
|
+
*
|
|
845
|
+
* @param name - Environment variable name
|
|
846
|
+
* @param defaultValue - Optional default value (if provided, won't throw)
|
|
847
|
+
* @returns The environment variable value
|
|
848
|
+
* @throws {Error} If variable is missing and no default provided
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* ```typescript
|
|
852
|
+
* const jwtSecret = requireEnv('JWT_SECRET');
|
|
853
|
+
* const port = requireEnv('PORT', '4000');
|
|
854
|
+
* ```
|
|
855
|
+
*/
|
|
856
|
+
declare function requireEnv(name: string, defaultValue?: string): string;
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Options for customizing field generation.
|
|
860
|
+
*/
|
|
861
|
+
interface ZodToAdminFieldsOptions {
|
|
862
|
+
/** Fields to exclude from the result */
|
|
863
|
+
hiddenFields?: string[];
|
|
864
|
+
/** Override which fields appear in table */
|
|
865
|
+
tableColumns?: string[];
|
|
866
|
+
/** Override specific field configurations */
|
|
867
|
+
fieldOverrides?: Partial<Record<string, Partial<AdminFieldConfig>>>;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Convert a Zod object schema to AdminFieldConfig array.
|
|
871
|
+
*
|
|
872
|
+
* @param schema - A Zod object schema (z.object({ ... }))
|
|
873
|
+
* @param options - Optional customization
|
|
874
|
+
* @returns Array of AdminFieldConfig for each field
|
|
875
|
+
*
|
|
876
|
+
* @example
|
|
877
|
+
* ```typescript
|
|
878
|
+
* const userSchema = z.object({
|
|
879
|
+
* id: z.string(),
|
|
880
|
+
* name: z.string().optional(),
|
|
881
|
+
* email: z.string().email(),
|
|
882
|
+
* role: z.enum(["user", "admin"]),
|
|
883
|
+
* createdAt: z.date(),
|
|
884
|
+
* });
|
|
885
|
+
*
|
|
886
|
+
* const fields = zodToAdminFields(userSchema);
|
|
887
|
+
* // Returns field configs with appropriate types, labels, editability
|
|
888
|
+
* ```
|
|
889
|
+
*/
|
|
890
|
+
declare function zodToAdminFields(schema: z.ZodTypeAny, options?: ZodToAdminFieldsOptions): AdminFieldConfig[];
|
|
891
|
+
/**
|
|
892
|
+
* Generate AdminFieldConfig for common entity fields that aren't in the create schema.
|
|
893
|
+
* These are typically auto-generated fields like id, createdAt, updatedAt.
|
|
894
|
+
*/
|
|
895
|
+
declare function getDefaultEntityFields(): AdminFieldConfig[];
|
|
896
|
+
/**
|
|
897
|
+
* Merge schema-derived fields with default entity fields.
|
|
898
|
+
* Default fields come first (id, createdAt, updatedAt), then schema fields.
|
|
899
|
+
*/
|
|
900
|
+
declare function mergeWithDefaultFields(schemaFields: AdminFieldConfig[], options?: {
|
|
901
|
+
includeDefaults?: string[];
|
|
902
|
+
}): AdminFieldConfig[];
|
|
903
|
+
|
|
904
|
+
export { AdminListPayload, AdminListResponse, AdminSetACLPayload, AdminSubscribersResponse, BaseService, BaseServiceInstance, BaseServiceOptions, type DiscordUser, type EnvValidationResult, type GoogleUser, InstallAdminMethodsOptions, type JWTPayload, type OAuthConfig, type OAuthProvider, type OAuthTokenResponse, PrismaDelegate, QuickdrawServerOptions, QuickdrawServerResult, QuickdrawSocket, type RateLimitOptions, type RateLimiter, type RedisAdapterOptions, type RedisAdapterResult, ServiceRegistry, ServiceRegistryInstance, type ServiceRegistryOptions, type ValidateEnvOptions, type ZodToAdminFieldsOptions, applyRateLimitMiddleware, checkEnv, createJWT, createOAuthURL, createQuickdrawServer, createRateLimiter, createTieredRateLimiter, discordProvider, exchangeOAuthCode, getDefaultEntityFields, googleProvider, isRedisAdapterAvailable, mergeWithDefaultFields, requireEnv, setupRedisAdapter, validateEnv, verifyJWT, zodToAdminFields };
|
package/dist/server/index.js
CHANGED
|
@@ -12,6 +12,168 @@ var consoleLogger = {
|
|
|
12
12
|
child: () => consoleLogger
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
// src/server/utils/zodToAdminFields.ts
|
|
16
|
+
var NON_EDITABLE_FIELDS = /* @__PURE__ */ new Set([
|
|
17
|
+
"id",
|
|
18
|
+
"createdAt",
|
|
19
|
+
"updatedAt",
|
|
20
|
+
"created_at",
|
|
21
|
+
"updated_at"
|
|
22
|
+
]);
|
|
23
|
+
var DEFAULT_HIDDEN_FIELDS = /* @__PURE__ */ new Set([
|
|
24
|
+
"acl",
|
|
25
|
+
"serviceAccess",
|
|
26
|
+
"service_access"
|
|
27
|
+
]);
|
|
28
|
+
function toLabel(fieldName) {
|
|
29
|
+
return fieldName.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
30
|
+
}
|
|
31
|
+
function getFieldType(zodType) {
|
|
32
|
+
const typeName = zodType._def.typeName;
|
|
33
|
+
if (typeName === "ZodOptional" || typeName === "ZodNullable" || typeName === "ZodDefault") {
|
|
34
|
+
const inner = zodType._def.innerType;
|
|
35
|
+
return getFieldType(inner);
|
|
36
|
+
}
|
|
37
|
+
switch (typeName) {
|
|
38
|
+
case "ZodString":
|
|
39
|
+
return "string";
|
|
40
|
+
case "ZodNumber":
|
|
41
|
+
return "number";
|
|
42
|
+
case "ZodBoolean":
|
|
43
|
+
return "boolean";
|
|
44
|
+
case "ZodDate":
|
|
45
|
+
return "date";
|
|
46
|
+
case "ZodEnum":
|
|
47
|
+
case "ZodNativeEnum":
|
|
48
|
+
return "enum";
|
|
49
|
+
case "ZodObject":
|
|
50
|
+
case "ZodArray":
|
|
51
|
+
case "ZodRecord":
|
|
52
|
+
return "json";
|
|
53
|
+
default:
|
|
54
|
+
return "string";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function getEnumValues(zodType) {
|
|
58
|
+
const typeName = zodType._def.typeName;
|
|
59
|
+
if (typeName === "ZodOptional" || typeName === "ZodNullable" || typeName === "ZodDefault") {
|
|
60
|
+
const inner = zodType._def.innerType;
|
|
61
|
+
return getEnumValues(inner);
|
|
62
|
+
}
|
|
63
|
+
if (typeName === "ZodEnum") {
|
|
64
|
+
return zodType._def.values;
|
|
65
|
+
}
|
|
66
|
+
if (typeName === "ZodNativeEnum") {
|
|
67
|
+
const enumObj = zodType._def.values;
|
|
68
|
+
return Object.values(enumObj).filter(
|
|
69
|
+
(v) => typeof v === "string"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
74
|
+
function isOptional(zodType) {
|
|
75
|
+
const typeName = zodType._def.typeName;
|
|
76
|
+
if (typeName === "ZodOptional" || typeName === "ZodNullable") {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (typeName === "ZodDefault") {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
function zodToAdminFields(schema, options = {}) {
|
|
85
|
+
const { hiddenFields = [], tableColumns, fieldOverrides = {} } = options;
|
|
86
|
+
let shape = {};
|
|
87
|
+
let currentSchema = schema;
|
|
88
|
+
while (currentSchema) {
|
|
89
|
+
const typeName = currentSchema._def.typeName;
|
|
90
|
+
if (typeName === "ZodObject") {
|
|
91
|
+
shape = currentSchema._def.shape();
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
if (typeName === "ZodEffects" || typeName === "ZodOptional" || typeName === "ZodNullable") {
|
|
95
|
+
currentSchema = currentSchema._def.schema || currentSchema._def.innerType;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
const hiddenSet = /* @__PURE__ */ new Set([...DEFAULT_HIDDEN_FIELDS, ...hiddenFields]);
|
|
101
|
+
const fields = [];
|
|
102
|
+
for (const [name, zodType] of Object.entries(shape)) {
|
|
103
|
+
if (hiddenSet.has(name)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const fieldType = getFieldType(zodType);
|
|
107
|
+
const required = !isOptional(zodType);
|
|
108
|
+
const editable = !NON_EDITABLE_FIELDS.has(name);
|
|
109
|
+
const enumValues = getEnumValues(zodType);
|
|
110
|
+
let showInTable = true;
|
|
111
|
+
if (tableColumns) {
|
|
112
|
+
showInTable = tableColumns.includes(name);
|
|
113
|
+
} else {
|
|
114
|
+
showInTable = fieldType !== "json";
|
|
115
|
+
}
|
|
116
|
+
const baseConfig = {
|
|
117
|
+
name,
|
|
118
|
+
type: fieldType,
|
|
119
|
+
label: toLabel(name),
|
|
120
|
+
required,
|
|
121
|
+
editable,
|
|
122
|
+
showInTable,
|
|
123
|
+
sortable: fieldType !== "json" && fieldType !== "relation",
|
|
124
|
+
enumValues
|
|
125
|
+
};
|
|
126
|
+
const override = fieldOverrides[name];
|
|
127
|
+
if (override) {
|
|
128
|
+
Object.assign(baseConfig, override);
|
|
129
|
+
}
|
|
130
|
+
fields.push(baseConfig);
|
|
131
|
+
}
|
|
132
|
+
return fields;
|
|
133
|
+
}
|
|
134
|
+
function getDefaultEntityFields() {
|
|
135
|
+
return [
|
|
136
|
+
{
|
|
137
|
+
name: "id",
|
|
138
|
+
type: "string",
|
|
139
|
+
label: "ID",
|
|
140
|
+
required: true,
|
|
141
|
+
editable: false,
|
|
142
|
+
showInTable: true,
|
|
143
|
+
sortable: true
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "createdAt",
|
|
147
|
+
type: "date",
|
|
148
|
+
label: "Created At",
|
|
149
|
+
required: true,
|
|
150
|
+
editable: false,
|
|
151
|
+
showInTable: true,
|
|
152
|
+
sortable: true
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "updatedAt",
|
|
156
|
+
type: "date",
|
|
157
|
+
label: "Updated At",
|
|
158
|
+
required: true,
|
|
159
|
+
editable: false,
|
|
160
|
+
showInTable: false,
|
|
161
|
+
sortable: true
|
|
162
|
+
}
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
function mergeWithDefaultFields(schemaFields, options = {}) {
|
|
166
|
+
const { includeDefaults = ["id", "createdAt"] } = options;
|
|
167
|
+
const defaultFields = getDefaultEntityFields().filter(
|
|
168
|
+
(f) => includeDefaults.includes(f.name)
|
|
169
|
+
);
|
|
170
|
+
const defaultNames = new Set(defaultFields.map((f) => f.name));
|
|
171
|
+
const filteredSchemaFields = schemaFields.filter(
|
|
172
|
+
(f) => !defaultNames.has(f.name)
|
|
173
|
+
);
|
|
174
|
+
return [...defaultFields, ...filteredSchemaFields];
|
|
175
|
+
}
|
|
176
|
+
|
|
15
177
|
// src/server/BaseService.ts
|
|
16
178
|
var BaseService = class {
|
|
17
179
|
serviceName;
|
|
@@ -26,6 +188,8 @@ var BaseService = class {
|
|
|
26
188
|
io = null;
|
|
27
189
|
// Collection of public methods for registry discovery
|
|
28
190
|
publicMethods = /* @__PURE__ */ new Map();
|
|
191
|
+
// Admin metadata configuration (set by installAdminMethods)
|
|
192
|
+
adminMeta = null;
|
|
29
193
|
constructor(options) {
|
|
30
194
|
this.serviceName = options.serviceName;
|
|
31
195
|
this.hasEntryACL = options.hasEntryACL ?? false;
|
|
@@ -448,11 +612,50 @@ var BaseService = class {
|
|
|
448
612
|
* reemit: "Admin",
|
|
449
613
|
* unsubscribeAll: "Admin",
|
|
450
614
|
* },
|
|
615
|
+
* schema: createEntitySchema,
|
|
616
|
+
* displayName: "Entities",
|
|
451
617
|
* });
|
|
452
618
|
* ```
|
|
453
619
|
*/
|
|
454
620
|
installAdminMethods(options) {
|
|
455
|
-
const {
|
|
621
|
+
const {
|
|
622
|
+
expose,
|
|
623
|
+
access,
|
|
624
|
+
schema,
|
|
625
|
+
displayName,
|
|
626
|
+
tableColumns,
|
|
627
|
+
hiddenFields,
|
|
628
|
+
fieldOverrides
|
|
629
|
+
} = options;
|
|
630
|
+
if (schema) {
|
|
631
|
+
const schemaFields = zodToAdminFields(schema, {
|
|
632
|
+
hiddenFields,
|
|
633
|
+
tableColumns,
|
|
634
|
+
fieldOverrides
|
|
635
|
+
});
|
|
636
|
+
const fields = mergeWithDefaultFields(schemaFields);
|
|
637
|
+
this.adminMeta = {
|
|
638
|
+
serviceName: this.serviceName,
|
|
639
|
+
displayName: displayName ?? this.toDisplayName(this.serviceName),
|
|
640
|
+
fields
|
|
641
|
+
};
|
|
642
|
+
} else {
|
|
643
|
+
this.adminMeta = {
|
|
644
|
+
serviceName: this.serviceName,
|
|
645
|
+
displayName: displayName ?? this.toDisplayName(this.serviceName),
|
|
646
|
+
fields: []
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
const shouldExposeMeta = expose.meta ?? (expose.list || expose.get || expose.create || expose.update || expose.delete);
|
|
650
|
+
if (shouldExposeMeta) {
|
|
651
|
+
this.publicMethods.set("adminMeta", {
|
|
652
|
+
name: "adminMeta",
|
|
653
|
+
access: access.meta ?? access.list,
|
|
654
|
+
handler: async (_payload, _context) => {
|
|
655
|
+
return this.getAdminMeta();
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
456
659
|
if (expose.list) {
|
|
457
660
|
this.publicMethods.set("adminList", {
|
|
458
661
|
name: "adminList",
|
|
@@ -550,9 +753,30 @@ var BaseService = class {
|
|
|
550
753
|
resolveEntryId: (payload) => payload.entryId
|
|
551
754
|
});
|
|
552
755
|
}
|
|
553
|
-
|
|
554
|
-
|
|
756
|
+
const installedMethods = Object.keys(expose).filter(
|
|
757
|
+
(k) => expose[k]
|
|
555
758
|
);
|
|
759
|
+
if (shouldExposeMeta) installedMethods.push("meta");
|
|
760
|
+
this.logger.info(`Installed admin methods: ${installedMethods.join(", ")}`);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Convert service name to display name.
|
|
764
|
+
* e.g., "chatService" -> "Chats", "userService" -> "Users"
|
|
765
|
+
*/
|
|
766
|
+
toDisplayName(serviceName) {
|
|
767
|
+
return serviceName.replace(/Service$/i, "").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase()) + "s";
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Get admin metadata for this service.
|
|
771
|
+
* Returns the configured metadata or throws if not configured.
|
|
772
|
+
*/
|
|
773
|
+
getAdminMeta() {
|
|
774
|
+
if (!this.adminMeta) {
|
|
775
|
+
throw new Error(
|
|
776
|
+
`Admin methods not installed for ${this.serviceName}. Call installAdminMethods() first.`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
return this.adminMeta;
|
|
556
780
|
}
|
|
557
781
|
/**
|
|
558
782
|
* Admin method: List entities with pagination and filters.
|
|
@@ -687,12 +911,24 @@ var ServiceRegistry = class {
|
|
|
687
911
|
io;
|
|
688
912
|
services = /* @__PURE__ */ new Map();
|
|
689
913
|
logger;
|
|
914
|
+
methodLoggingEnabled;
|
|
915
|
+
logPayloads;
|
|
916
|
+
logResponses;
|
|
690
917
|
connectionHandlerInstalled = false;
|
|
691
918
|
// Store method metadata for deferred registration
|
|
692
919
|
methodRegistry = /* @__PURE__ */ new Map();
|
|
693
920
|
constructor(io, options) {
|
|
694
921
|
this.io = io;
|
|
695
922
|
this.logger = options?.logger?.child({ service: "ServiceRegistry" }) ?? consoleLogger.child({ service: "ServiceRegistry" });
|
|
923
|
+
this.methodLoggingEnabled = options?.methodLogging?.enabled ?? true;
|
|
924
|
+
this.logPayloads = options?.methodLogging?.logPayloads ?? false;
|
|
925
|
+
this.logResponses = options?.methodLogging?.logResponses ?? false;
|
|
926
|
+
if (this.methodLoggingEnabled) {
|
|
927
|
+
this.logger.info("Automatic method logging enabled", {
|
|
928
|
+
logPayloads: this.logPayloads,
|
|
929
|
+
logResponses: this.logResponses
|
|
930
|
+
});
|
|
931
|
+
}
|
|
696
932
|
}
|
|
697
933
|
/**
|
|
698
934
|
* Install the single connection handler that wires all registered services.
|
|
@@ -798,33 +1034,45 @@ var ServiceRegistry = class {
|
|
|
798
1034
|
}
|
|
799
1035
|
}
|
|
800
1036
|
await service.ensureAccessForMethod(method.access, socket, entryId);
|
|
801
|
-
this.
|
|
802
|
-
|
|
803
|
-
{
|
|
1037
|
+
if (this.methodLoggingEnabled) {
|
|
1038
|
+
const logContext = {
|
|
804
1039
|
category: "request_processing",
|
|
805
1040
|
serviceName,
|
|
806
1041
|
methodName,
|
|
807
1042
|
userId: socket.userId,
|
|
808
1043
|
socketId: socket.id
|
|
1044
|
+
};
|
|
1045
|
+
if (this.logPayloads) {
|
|
1046
|
+
logContext.payload = validatedPayload;
|
|
809
1047
|
}
|
|
810
|
-
|
|
1048
|
+
this.logger.info(
|
|
1049
|
+
`User ${userIdShort} ${serviceName}.${methodName} -> start`,
|
|
1050
|
+
logContext
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
811
1053
|
const result = await method.handler(validatedPayload, {
|
|
812
1054
|
userId: socket.userId,
|
|
813
1055
|
socketId: socket.id,
|
|
814
1056
|
serviceAccess: socket.serviceAccess ?? {}
|
|
815
1057
|
});
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
{
|
|
1058
|
+
if (this.methodLoggingEnabled) {
|
|
1059
|
+
const durationMs = Date.now() - startedAt;
|
|
1060
|
+
const logContext = {
|
|
820
1061
|
category: "request_processing",
|
|
821
1062
|
outcome: "success",
|
|
822
1063
|
durationMs,
|
|
823
1064
|
serviceName,
|
|
824
1065
|
methodName,
|
|
825
1066
|
userId: socket.userId
|
|
1067
|
+
};
|
|
1068
|
+
if (this.logResponses) {
|
|
1069
|
+
logContext.response = result;
|
|
826
1070
|
}
|
|
827
|
-
|
|
1071
|
+
this.logger.info(
|
|
1072
|
+
`User ${userIdShort} ${serviceName}.${methodName} -> success`,
|
|
1073
|
+
logContext
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
828
1076
|
const successResponse = {
|
|
829
1077
|
success: true,
|
|
830
1078
|
data: result
|
|
@@ -832,14 +1080,22 @@ var ServiceRegistry = class {
|
|
|
832
1080
|
callback?.(successResponse);
|
|
833
1081
|
} catch (error) {
|
|
834
1082
|
const durationMs = Date.now() - startedAt;
|
|
1083
|
+
const logContext = {
|
|
1084
|
+
category: "request_processing",
|
|
1085
|
+
outcome: "fail",
|
|
1086
|
+
durationMs,
|
|
1087
|
+
serviceName,
|
|
1088
|
+
methodName,
|
|
1089
|
+
userId: socket.userId,
|
|
1090
|
+
socketId: socket.id,
|
|
1091
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1092
|
+
};
|
|
1093
|
+
if (this.logPayloads) {
|
|
1094
|
+
logContext.payload = payload;
|
|
1095
|
+
}
|
|
835
1096
|
this.logger.error(
|
|
836
1097
|
`User ${userIdShort} ${serviceName}.${methodName} -> fail`,
|
|
837
|
-
|
|
838
|
-
category: "request_processing",
|
|
839
|
-
outcome: "fail",
|
|
840
|
-
durationMs,
|
|
841
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
842
|
-
}
|
|
1098
|
+
logContext
|
|
843
1099
|
);
|
|
844
1100
|
const errorResponse = {
|
|
845
1101
|
success: false,
|
|
@@ -947,7 +1203,10 @@ function createQuickdrawServer(options) {
|
|
|
947
1203
|
credentials: options.cors?.credentials ?? true
|
|
948
1204
|
}
|
|
949
1205
|
});
|
|
950
|
-
const registry = new ServiceRegistry(io, {
|
|
1206
|
+
const registry = new ServiceRegistry(io, {
|
|
1207
|
+
logger,
|
|
1208
|
+
methodLogging: options.methodLogging
|
|
1209
|
+
});
|
|
951
1210
|
io.use(async (socket, next) => {
|
|
952
1211
|
const quickdrawSocket = socket;
|
|
953
1212
|
try {
|
|
@@ -994,6 +1253,34 @@ function createQuickdrawServer(options) {
|
|
|
994
1253
|
`Registered services: ${registry.getServices().join(", ")}`
|
|
995
1254
|
);
|
|
996
1255
|
});
|
|
1256
|
+
const shutdown = async (signal) => {
|
|
1257
|
+
serverLogger.info(`Received ${signal}, starting graceful shutdown...`);
|
|
1258
|
+
httpServer.close(() => {
|
|
1259
|
+
serverLogger.info("HTTP server closed");
|
|
1260
|
+
});
|
|
1261
|
+
const sockets = await io.fetchSockets();
|
|
1262
|
+
serverLogger.info(`Closing ${sockets.length} active socket connections...`);
|
|
1263
|
+
for (const socket of sockets) {
|
|
1264
|
+
const quickdrawSocket = socket;
|
|
1265
|
+
for (const service of registry.getServiceInstances()) {
|
|
1266
|
+
try {
|
|
1267
|
+
service.unsubscribeSocket(quickdrawSocket);
|
|
1268
|
+
} catch {
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
socket.disconnect(true);
|
|
1272
|
+
}
|
|
1273
|
+
io.close(() => {
|
|
1274
|
+
serverLogger.info("Socket.io server closed");
|
|
1275
|
+
process.exit(0);
|
|
1276
|
+
});
|
|
1277
|
+
setTimeout(() => {
|
|
1278
|
+
serverLogger.error("Graceful shutdown timeout, forcing exit");
|
|
1279
|
+
process.exit(1);
|
|
1280
|
+
}, 1e4);
|
|
1281
|
+
};
|
|
1282
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
1283
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
997
1284
|
return { io, httpServer, registry };
|
|
998
1285
|
}
|
|
999
1286
|
|
|
@@ -1314,6 +1601,45 @@ var googleProvider = {
|
|
|
1314
1601
|
}
|
|
1315
1602
|
};
|
|
1316
1603
|
|
|
1317
|
-
|
|
1604
|
+
// src/server/utils/env.ts
|
|
1605
|
+
function validateEnv(options) {
|
|
1606
|
+
const result = checkEnv(options);
|
|
1607
|
+
if (!result.success) {
|
|
1608
|
+
throw new Error(result.message);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
function checkEnv(options) {
|
|
1612
|
+
const { required, productionOnly = true, errorPrefix = "Missing required environment variables" } = options;
|
|
1613
|
+
if (productionOnly && process.env.NODE_ENV !== "production") {
|
|
1614
|
+
return { success: true, missing: [] };
|
|
1615
|
+
}
|
|
1616
|
+
const missing = [];
|
|
1617
|
+
for (const varName of required) {
|
|
1618
|
+
const value = process.env[varName];
|
|
1619
|
+
if (!value || value.trim() === "") {
|
|
1620
|
+
missing.push(varName);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (missing.length > 0) {
|
|
1624
|
+
return {
|
|
1625
|
+
success: false,
|
|
1626
|
+
missing,
|
|
1627
|
+
message: `${errorPrefix}: ${missing.join(", ")}`
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
return { success: true, missing: [] };
|
|
1631
|
+
}
|
|
1632
|
+
function requireEnv(name, defaultValue) {
|
|
1633
|
+
const value = process.env[name];
|
|
1634
|
+
if (!value || value.trim() === "") {
|
|
1635
|
+
if (defaultValue !== void 0) {
|
|
1636
|
+
return defaultValue;
|
|
1637
|
+
}
|
|
1638
|
+
throw new Error(`Environment variable ${name} is required but not set`);
|
|
1639
|
+
}
|
|
1640
|
+
return value;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
export { BaseService, ServiceRegistry, applyRateLimitMiddleware, checkEnv, createJWT, createOAuthURL, createQuickdrawServer, createRateLimiter, createTieredRateLimiter, discordProvider, exchangeOAuthCode, getDefaultEntityFields, googleProvider, isRedisAdapterAvailable, mergeWithDefaultFields, requireEnv, setupRedisAdapter, validateEnv, verifyJWT, zodToAdminFields };
|
|
1318
1644
|
//# sourceMappingURL=index.js.map
|
|
1319
1645
|
//# sourceMappingURL=index.js.map
|