@minesa-org/mini-interaction 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -1,3 +1,7 @@
1
1
  # Mini Interaction
2
2
 
3
3
  Mini interaction, connecting your app with Discord via HTTP-interaction (Vercel support).
4
+
5
+ - Read the [Discord OAuth Linked Roles setup guide](docs/discord-oauth-setup.md)
6
+ for a step-by-step walkthrough that uses `mini.discordOAuthCallback()` and a
7
+ MongoDB connection configured via `MONGODB_URI`.
@@ -4,6 +4,7 @@ import type { MiniInteractionCommand } from "../types/Commands.js";
4
4
  import { RoleConnectionMetadataTypes } from "../types/RoleConnectionMetadataTypes.js";
5
5
  import { type MessageComponentInteraction, type ButtonInteraction, type StringSelectInteraction, type RoleSelectInteraction, type UserSelectInteraction, type ChannelSelectInteraction, type MentionableSelectInteraction } from "../utils/MessageComponentInteraction.js";
6
6
  import { type ModalSubmitInteraction } from "../utils/ModalSubmitInteraction.js";
7
+ import { type OAuthConfig, type OAuthTokens, type DiscordUser } from "../oauth/DiscordOAuth.js";
7
8
  /** Configuration parameters for the MiniInteraction client. */
8
9
  export type MiniInteractionOptions = {
9
10
  applicationId: string;
@@ -73,6 +74,45 @@ export type MiniInteractionModal = {
73
74
  export type MiniInteractionNodeHandler = (request: IncomingMessage, response: ServerResponse) => void;
74
75
  /** Web Fetch API compatible request handler for platforms such as Cloudflare Workers. */
75
76
  export type MiniInteractionFetchHandler = (request: Request) => Promise<Response>;
77
+ /** Context passed to OAuth success handlers and templates. */
78
+ export type DiscordOAuthAuthorizeContext = {
79
+ tokens: OAuthTokens;
80
+ user: DiscordUser;
81
+ state: string | null;
82
+ request: IncomingMessage;
83
+ response: ServerResponse;
84
+ };
85
+ /** Context provided to templates that only need access to the state value. */
86
+ export type DiscordOAuthStateTemplateContext = {
87
+ state: string | null;
88
+ };
89
+ /** Context provided to templates that display OAuth error messages. */
90
+ export type DiscordOAuthErrorTemplateContext = DiscordOAuthStateTemplateContext & {
91
+ error: string;
92
+ };
93
+ /** Context provided to templates used for successful authorisation messages. */
94
+ export type DiscordOAuthSuccessTemplateContext = DiscordOAuthStateTemplateContext & {
95
+ user: DiscordUser;
96
+ tokens: OAuthTokens;
97
+ };
98
+ /** Context provided to templates that handle server side failures. */
99
+ export type DiscordOAuthServerErrorTemplateContext = DiscordOAuthStateTemplateContext;
100
+ /** Template functions used to render HTML responses during the OAuth flow. */
101
+ export type DiscordOAuthCallbackTemplates = {
102
+ success: (context: DiscordOAuthSuccessTemplateContext) => string;
103
+ missingCode: (context: DiscordOAuthStateTemplateContext) => string;
104
+ oauthError: (context: DiscordOAuthErrorTemplateContext) => string;
105
+ invalidState: (context: DiscordOAuthStateTemplateContext) => string;
106
+ serverError: (context: DiscordOAuthServerErrorTemplateContext) => string;
107
+ };
108
+ /** Options accepted by {@link MiniInteraction.discordOAuthCallback}. */
109
+ export type DiscordOAuthCallbackOptions = {
110
+ oauth?: OAuthConfig;
111
+ onAuthorize?: (context: DiscordOAuthAuthorizeContext) => Promise<void> | void;
112
+ validateState?: (state: string | null, request: IncomingMessage) => Promise<boolean> | boolean;
113
+ successRedirect?: string | ((context: DiscordOAuthAuthorizeContext) => string | null | undefined);
114
+ templates?: Partial<DiscordOAuthCallbackTemplates>;
115
+ };
76
116
  /**
77
117
  * Minimal interface describing a function capable of verifying Discord interaction signatures.
78
118
  */
@@ -91,6 +131,7 @@ export declare class MiniInteraction {
91
131
  private readonly commands;
92
132
  private readonly componentHandlers;
93
133
  private readonly modalHandlers;
134
+ private readonly htmlTemplateCache;
94
135
  private commandsLoaded;
95
136
  private loadCommandsPromise;
96
137
  private componentsLoaded;
@@ -183,6 +224,38 @@ export declare class MiniInteraction {
183
224
  * Convenience alias for {@link createNodeHandler} tailored to Vercel serverless functions.
184
225
  */
185
226
  createVercelHandler(): MiniInteractionNodeHandler;
227
+ /**
228
+ * Loads an HTML file and returns a success template that replaces useful placeholders.
229
+ *
230
+ * The following placeholders are available in the HTML file:
231
+ * - `{{username}}`, `{{discriminator}}`, `{{user_id}}`, `{{user_tag}}`
232
+ * - `{{access_token}}`, `{{refresh_token}}`, `{{token_type}}`, `{{scope}}`, `{{expires_at}}`
233
+ * - `{{state}}`
234
+ */
235
+ connectedOAuthPage(filePath: string): DiscordOAuthCallbackTemplates["success"];
236
+ /**
237
+ * Loads an HTML file and returns an error template that can be reused for all failure cases.
238
+ *
239
+ * The following placeholders are available in the HTML file:
240
+ * - `{{error}}`
241
+ * - `{{state}}`
242
+ */
243
+ failedOAuthPage(filePath: string): ((context: DiscordOAuthErrorTemplateContext) => string) & ((context: DiscordOAuthStateTemplateContext) => string);
244
+ /**
245
+ * Resolves an HTML template from disk and caches the result for reuse.
246
+ */
247
+ private loadHtmlTemplate;
248
+ /**
249
+ * Replaces placeholder tokens in a template with escaped HTML values.
250
+ */
251
+ private renderHtmlTemplate;
252
+ /**
253
+ * Creates a minimal Discord OAuth callback handler that renders helpful HTML responses.
254
+ *
255
+ * This helper keeps the user-side implementation tiny while still exposing hooks for
256
+ * storing metadata or validating the OAuth state value.
257
+ */
258
+ discordOAuthCallback(options: DiscordOAuthCallbackOptions): MiniInteractionNodeHandler;
186
259
  /**
187
260
  * Creates a Fetch API compatible handler for runtimes like Workers or Deno.
188
261
  */
@@ -1,4 +1,4 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { readdir, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
@@ -9,6 +9,7 @@ import { createCommandInteraction } from "../utils/CommandInteractionOptions.js"
9
9
  import { createMessageComponentInteraction, } from "../utils/MessageComponentInteraction.js";
10
10
  import { createModalSubmitInteraction, } from "../utils/ModalSubmitInteraction.js";
11
11
  import { createUserContextMenuInteraction, createMessageContextMenuInteraction, } from "../utils/ContextMenuInteraction.js";
12
+ import { getOAuthTokens, getDiscordUser, } from "../oauth/DiscordOAuth.js";
12
13
  /** File extensions that are treated as loadable modules when auto-loading. */
13
14
  const SUPPORTED_MODULE_EXTENSIONS = new Set([
14
15
  ".js",
@@ -32,6 +33,7 @@ export class MiniInteraction {
32
33
  commands = new Map();
33
34
  componentHandlers = new Map();
34
35
  modalHandlers = new Map();
36
+ htmlTemplateCache = new Map();
35
37
  commandsLoaded = false;
36
38
  loadCommandsPromise = null;
37
39
  componentsLoaded = false;
@@ -435,6 +437,172 @@ export class MiniInteraction {
435
437
  createVercelHandler() {
436
438
  return this.createNodeHandler();
437
439
  }
440
+ /**
441
+ * Loads an HTML file and returns a success template that replaces useful placeholders.
442
+ *
443
+ * The following placeholders are available in the HTML file:
444
+ * - `{{username}}`, `{{discriminator}}`, `{{user_id}}`, `{{user_tag}}`
445
+ * - `{{access_token}}`, `{{refresh_token}}`, `{{token_type}}`, `{{scope}}`, `{{expires_at}}`
446
+ * - `{{state}}`
447
+ */
448
+ connectedOAuthPage(filePath) {
449
+ const template = this.loadHtmlTemplate(filePath);
450
+ return ({ user, tokens, state }) => {
451
+ const discriminator = user.discriminator;
452
+ const userTag = discriminator && discriminator !== "0"
453
+ ? `${user.username}#${discriminator}`
454
+ : user.username;
455
+ return this.renderHtmlTemplate(template, {
456
+ username: user.username,
457
+ discriminator,
458
+ user_id: user.id,
459
+ user_tag: userTag,
460
+ access_token: tokens.access_token,
461
+ refresh_token: tokens.refresh_token,
462
+ token_type: tokens.token_type,
463
+ scope: tokens.scope,
464
+ expires_at: tokens.expires_at.toString(),
465
+ state,
466
+ });
467
+ };
468
+ }
469
+ /**
470
+ * Loads an HTML file and returns an error template that can be reused for all failure cases.
471
+ *
472
+ * The following placeholders are available in the HTML file:
473
+ * - `{{error}}`
474
+ * - `{{state}}`
475
+ */
476
+ failedOAuthPage(filePath) {
477
+ const template = this.loadHtmlTemplate(filePath);
478
+ const renderer = (context) => this.renderHtmlTemplate(template, {
479
+ error: context.error,
480
+ state: context.state,
481
+ });
482
+ return renderer;
483
+ }
484
+ /**
485
+ * Resolves an HTML template from disk and caches the result for reuse.
486
+ */
487
+ loadHtmlTemplate(filePath) {
488
+ const resolvedPath = path.isAbsolute(filePath)
489
+ ? filePath
490
+ : path.join(process.cwd(), filePath);
491
+ const cached = this.htmlTemplateCache.get(resolvedPath);
492
+ if (cached) {
493
+ return cached;
494
+ }
495
+ if (!existsSync(resolvedPath)) {
496
+ throw new Error(`[MiniInteraction] HTML template not found: ${resolvedPath}`);
497
+ }
498
+ const fileContents = readFileSync(resolvedPath, "utf8");
499
+ this.htmlTemplateCache.set(resolvedPath, fileContents);
500
+ return fileContents;
501
+ }
502
+ /**
503
+ * Replaces placeholder tokens in a template with escaped HTML values.
504
+ */
505
+ renderHtmlTemplate(template, values) {
506
+ return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => {
507
+ const value = values[key];
508
+ if (value === undefined || value === null) {
509
+ return "";
510
+ }
511
+ return escapeHtml(String(value));
512
+ });
513
+ }
514
+ /**
515
+ * Creates a minimal Discord OAuth callback handler that renders helpful HTML responses.
516
+ *
517
+ * This helper keeps the user-side implementation tiny while still exposing hooks for
518
+ * storing metadata or validating the OAuth state value.
519
+ */
520
+ discordOAuthCallback(options) {
521
+ const templates = {
522
+ ...DEFAULT_DISCORD_OAUTH_TEMPLATES,
523
+ ...options.templates,
524
+ };
525
+ const oauthConfig = resolveOAuthConfig(options.oauth);
526
+ return async (request, response) => {
527
+ if (request.method !== "GET") {
528
+ response.statusCode = 405;
529
+ response.setHeader("content-type", "application/json");
530
+ response.end(JSON.stringify({
531
+ error: "[MiniInteraction] Only GET is supported",
532
+ }));
533
+ return;
534
+ }
535
+ let state = null;
536
+ try {
537
+ const host = request.headers.host ?? "localhost";
538
+ const url = new URL(request.url ?? "", `http://${host}`);
539
+ const code = url.searchParams.get("code");
540
+ state = url.searchParams.get("state");
541
+ const error = url.searchParams.get("error");
542
+ if (error) {
543
+ sendHtml(response, templates.oauthError({
544
+ error,
545
+ state,
546
+ }), 400);
547
+ return;
548
+ }
549
+ if (!code) {
550
+ sendHtml(response, templates.missingCode({
551
+ state,
552
+ }), 400);
553
+ return;
554
+ }
555
+ if (options.validateState) {
556
+ const isValid = await options.validateState(state, request);
557
+ if (!isValid) {
558
+ sendHtml(response, templates.invalidState({
559
+ state,
560
+ }), 400);
561
+ return;
562
+ }
563
+ }
564
+ const tokens = await getOAuthTokens(code, oauthConfig);
565
+ const user = await getDiscordUser(tokens.access_token);
566
+ const authorizeContext = {
567
+ tokens,
568
+ user,
569
+ state,
570
+ request,
571
+ response,
572
+ };
573
+ if (options.onAuthorize) {
574
+ await options.onAuthorize(authorizeContext);
575
+ if (response.writableEnded || response.headersSent) {
576
+ return;
577
+ }
578
+ }
579
+ if (options.successRedirect) {
580
+ const location = typeof options.successRedirect === "function"
581
+ ? options.successRedirect(authorizeContext)
582
+ : options.successRedirect;
583
+ if (location && !response.headersSent && !response.writableEnded) {
584
+ response.statusCode = 302;
585
+ response.setHeader("location", location);
586
+ response.end();
587
+ return;
588
+ }
589
+ }
590
+ sendHtml(response, templates.success({
591
+ user,
592
+ tokens,
593
+ state,
594
+ }));
595
+ }
596
+ catch (error) {
597
+ console.error("[MiniInteraction] Discord OAuth callback failed:", error);
598
+ if (!response.headersSent && !response.writableEnded) {
599
+ sendHtml(response, templates.serverError({
600
+ state,
601
+ }), 500);
602
+ }
603
+ }
604
+ };
605
+ }
438
606
  /**
439
607
  * Creates a Fetch API compatible handler for runtimes like Workers or Deno.
440
608
  */
@@ -950,3 +1118,151 @@ export class MiniInteraction {
950
1118
  }
951
1119
  }
952
1120
  }
1121
+ const DEFAULT_DISCORD_OAUTH_TEMPLATES = {
1122
+ success: ({ user }) => {
1123
+ const username = escapeHtml(user.username ?? "Discord User");
1124
+ const discriminator = user.discriminator && user.discriminator !== "0"
1125
+ ? `#${escapeHtml(user.discriminator)}`
1126
+ : "";
1127
+ return `<!DOCTYPE html>
1128
+ <html>
1129
+ <head>
1130
+ <meta charset="utf-8" />
1131
+ <title>Linked Role Connected</title>
1132
+ <style>
1133
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; text-align: center; }
1134
+ .success { color: #2e7d32; background: #e8f5e9; padding: 20px; border-radius: 10px; margin: 20px 0; }
1135
+ .info { background: #e3f2fd; padding: 15px; border-radius: 5px; margin: 20px 0; }
1136
+ h1 { color: #5865f2; }
1137
+ .user-info { margin: 10px 0; font-size: 18px; }
1138
+ </style>
1139
+ </head>
1140
+ <body>
1141
+ <h1>✅ Successfully Connected!</h1>
1142
+ <div class="success">
1143
+ <p><strong>Your Discord account has been linked!</strong></p>
1144
+ <div class="user-info">
1145
+ <p>👤 ${username}${discriminator}</p>
1146
+ </div>
1147
+ </div>
1148
+ <div class="info">
1149
+ <p>Your linked role metadata has been updated.</p>
1150
+ <p>You can now close this window and return to Discord.</p>
1151
+ </div>
1152
+ </body>
1153
+ </html>`;
1154
+ },
1155
+ missingCode: () => `<!DOCTYPE html>
1156
+ <html>
1157
+ <head>
1158
+ <meta charset="utf-8" />
1159
+ <title>Missing Authorization Code</title>
1160
+ <style>
1161
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
1162
+ .error { color: #d32f2f; background: #ffebee; padding: 15px; border-radius: 5px; }
1163
+ </style>
1164
+ </head>
1165
+ <body>
1166
+ <h1>Missing Authorization Code</h1>
1167
+ <div class="error">
1168
+ <p>No authorization code was provided.</p>
1169
+ </div>
1170
+ </body>
1171
+ </html>`,
1172
+ oauthError: ({ error }) => `<!DOCTYPE html>
1173
+ <html>
1174
+ <head>
1175
+ <meta charset="utf-8" />
1176
+ <title>OAuth Error</title>
1177
+ <style>
1178
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
1179
+ .error { color: #d32f2f; background: #ffebee; padding: 15px; border-radius: 5px; }
1180
+ </style>
1181
+ </head>
1182
+ <body>
1183
+ <h1>OAuth Error</h1>
1184
+ <div class="error">
1185
+ <p>Authorization failed: ${escapeHtml(error)}</p>
1186
+ <p>Please try again.</p>
1187
+ </div>
1188
+ </body>
1189
+ </html>`,
1190
+ invalidState: () => `<!DOCTYPE html>
1191
+ <html>
1192
+ <head>
1193
+ <meta charset="utf-8" />
1194
+ <title>Invalid Session</title>
1195
+ <style>
1196
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
1197
+ .error { color: #d32f2f; background: #ffebee; padding: 15px; border-radius: 5px; }
1198
+ </style>
1199
+ </head>
1200
+ <body>
1201
+ <h1>Invalid Session</h1>
1202
+ <div class="error">
1203
+ <p>The provided state value did not match an active session.</p>
1204
+ <p>Please restart the linking process.</p>
1205
+ </div>
1206
+ </body>
1207
+ </html>`,
1208
+ serverError: () => `<!DOCTYPE html>
1209
+ <html>
1210
+ <head>
1211
+ <meta charset="utf-8" />
1212
+ <title>Server Error</title>
1213
+ <style>
1214
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
1215
+ .error { color: #d32f2f; background: #ffebee; padding: 15px; border-radius: 5px; }
1216
+ </style>
1217
+ </head>
1218
+ <body>
1219
+ <h1>Server Error</h1>
1220
+ <div class="error">
1221
+ <p>An error occurred while processing your request.</p>
1222
+ <p>Please try again later.</p>
1223
+ </div>
1224
+ </body>
1225
+ </html>`,
1226
+ };
1227
+ function sendHtml(response, body, statusCode = 200) {
1228
+ if (response.headersSent || response.writableEnded) {
1229
+ return;
1230
+ }
1231
+ response.statusCode = statusCode;
1232
+ response.setHeader("content-type", "text/html; charset=utf-8");
1233
+ response.end(body);
1234
+ }
1235
+ function escapeHtml(value) {
1236
+ return value.replace(/[&<>"']/g, (character) => {
1237
+ switch (character) {
1238
+ case "&":
1239
+ return "&amp;";
1240
+ case "<":
1241
+ return "&lt;";
1242
+ case ">":
1243
+ return "&gt;";
1244
+ case '"':
1245
+ return "&quot;";
1246
+ case "'":
1247
+ return "&#39;";
1248
+ default:
1249
+ return character;
1250
+ }
1251
+ });
1252
+ }
1253
+ function resolveOAuthConfig(provided) {
1254
+ if (provided) {
1255
+ return provided;
1256
+ }
1257
+ const appId = process.env.DISCORD_APPLICATION_ID ?? process.env.DISCORD_CLIENT_ID;
1258
+ const appSecret = process.env.DISCORD_CLIENT_SECRET;
1259
+ const redirectUri = process.env.DISCORD_REDIRECT_URI;
1260
+ if (!appId || !appSecret || !redirectUri) {
1261
+ throw new Error("[MiniInteraction] Missing OAuth configuration. Provide options.oauth or set DISCORD_APPLICATION_ID, DISCORD_CLIENT_SECRET, and DISCORD_REDIRECT_URI environment variables.");
1262
+ }
1263
+ return {
1264
+ appId,
1265
+ appSecret,
1266
+ redirectUri,
1267
+ };
1268
+ }
@@ -0,0 +1,62 @@
1
+ import type { JSONEncodable } from "../builders/shared.js";
2
+ /**
3
+ * Represents a field in the data schema.
4
+ */
5
+ export interface DataField {
6
+ name: string;
7
+ type: "string" | "number" | "boolean" | "object" | "array";
8
+ required?: boolean;
9
+ default?: unknown;
10
+ description?: string;
11
+ }
12
+ /**
13
+ * Builder for defining data schemas in MiniDatabase.
14
+ * Provides a fluent API for developers to define their data structure.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const userSchema = new MiniDataBuilder()
19
+ * .addField("userId", "string", { required: true })
20
+ * .addField("username", "string", { required: true })
21
+ * .addField("coins", "number", { default: 0 })
22
+ * .addField("metadata", "object", { default: {} });
23
+ * ```
24
+ */
25
+ export declare class MiniDataBuilder implements JSONEncodable<Record<string, DataField>> {
26
+ private fields;
27
+ /**
28
+ * Adds a field to the data schema.
29
+ */
30
+ addField(name: string, type: "string" | "number" | "boolean" | "object" | "array", options?: {
31
+ required?: boolean;
32
+ default?: unknown;
33
+ description?: string;
34
+ }): this;
35
+ /**
36
+ * Removes a field from the schema.
37
+ */
38
+ removeField(name: string): this;
39
+ /**
40
+ * Gets a specific field definition.
41
+ */
42
+ getField(name: string): DataField | undefined;
43
+ /**
44
+ * Gets all fields in the schema.
45
+ */
46
+ getFields(): DataField[];
47
+ /**
48
+ * Validates data against the schema.
49
+ */
50
+ validate(data: Record<string, unknown>): {
51
+ valid: boolean;
52
+ errors: string[];
53
+ };
54
+ /**
55
+ * Applies default values to data.
56
+ */
57
+ applyDefaults(data: Record<string, unknown>): Record<string, unknown>;
58
+ /**
59
+ * Serializes the schema to JSON.
60
+ */
61
+ toJSON(): Record<string, DataField>;
62
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Builder for defining data schemas in MiniDatabase.
3
+ * Provides a fluent API for developers to define their data structure.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * const userSchema = new MiniDataBuilder()
8
+ * .addField("userId", "string", { required: true })
9
+ * .addField("username", "string", { required: true })
10
+ * .addField("coins", "number", { default: 0 })
11
+ * .addField("metadata", "object", { default: {} });
12
+ * ```
13
+ */
14
+ export class MiniDataBuilder {
15
+ fields = new Map();
16
+ /**
17
+ * Adds a field to the data schema.
18
+ */
19
+ addField(name, type, options) {
20
+ this.fields.set(name, {
21
+ name,
22
+ type,
23
+ required: options?.required ?? false,
24
+ default: options?.default,
25
+ description: options?.description,
26
+ });
27
+ return this;
28
+ }
29
+ /**
30
+ * Removes a field from the schema.
31
+ */
32
+ removeField(name) {
33
+ this.fields.delete(name);
34
+ return this;
35
+ }
36
+ /**
37
+ * Gets a specific field definition.
38
+ */
39
+ getField(name) {
40
+ return this.fields.get(name);
41
+ }
42
+ /**
43
+ * Gets all fields in the schema.
44
+ */
45
+ getFields() {
46
+ return Array.from(this.fields.values());
47
+ }
48
+ /**
49
+ * Validates data against the schema.
50
+ */
51
+ validate(data) {
52
+ const errors = [];
53
+ for (const field of this.fields.values()) {
54
+ const value = data[field.name];
55
+ // Check required fields
56
+ if (field.required && (value === undefined || value === null)) {
57
+ errors.push(`Field "${field.name}" is required`);
58
+ continue;
59
+ }
60
+ // Check type if value exists
61
+ if (value !== undefined && value !== null) {
62
+ const actualType = Array.isArray(value) ? "array" : typeof value;
63
+ if (actualType !== field.type) {
64
+ errors.push(`Field "${field.name}" must be of type "${field.type}", got "${actualType}"`);
65
+ }
66
+ }
67
+ }
68
+ return {
69
+ valid: errors.length === 0,
70
+ errors,
71
+ };
72
+ }
73
+ /**
74
+ * Applies default values to data.
75
+ */
76
+ applyDefaults(data) {
77
+ const result = { ...data };
78
+ for (const field of this.fields.values()) {
79
+ if (result[field.name] === undefined && field.default !== undefined) {
80
+ result[field.name] =
81
+ typeof field.default === "object"
82
+ ? JSON.parse(JSON.stringify(field.default))
83
+ : field.default;
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+ /**
89
+ * Serializes the schema to JSON.
90
+ */
91
+ toJSON() {
92
+ const result = {};
93
+ for (const [key, field] of this.fields) {
94
+ result[key] = field;
95
+ }
96
+ return result;
97
+ }
98
+ }
@@ -0,0 +1,66 @@
1
+ import type { MiniDataBuilder } from "./MiniDataBuilder.js";
2
+ import type { DatabaseConfig } from "./MiniDatabaseBuilder.js";
3
+ /**
4
+ * MiniDatabase provides async data storage backed by MongoDB.
5
+ * Designed to work seamlessly with Vercel and other serverless environments.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const db = new MiniDatabase(config, schema);
10
+ *
11
+ * // Get data
12
+ * const user = await db.get("user123");
13
+ *
14
+ * // Set data
15
+ * await db.set("user123", { username: "john", coins: 100 });
16
+ *
17
+ * // Update data
18
+ * await db.update("user123", { coins: 150 });
19
+ * ```
20
+ */
21
+ export declare class MiniDatabase {
22
+ private config;
23
+ private schema?;
24
+ private mongoClient?;
25
+ private mongoDb?;
26
+ private mongoCollection?;
27
+ private initPromise?;
28
+ /**
29
+ * Creates a MiniDatabase instance using an environment variable for the MongoDB URI.
30
+ * Defaults to the `MONGODB_URI` variable expected by Vercel and many hosting providers.
31
+ */
32
+ static fromEnv(schema?: MiniDataBuilder, options?: {
33
+ variable?: string;
34
+ dbName?: string;
35
+ collectionName?: string;
36
+ }): MiniDatabase;
37
+ constructor(config: DatabaseConfig, schema?: MiniDataBuilder);
38
+ /**
39
+ * Initializes the database connection.
40
+ */
41
+ private initialize;
42
+ /**
43
+ * Initializes MongoDB connection.
44
+ */
45
+ private initializeMongoDB;
46
+ /**
47
+ * Gets data by key.
48
+ */
49
+ get(key: string): Promise<Record<string, unknown> | null>;
50
+ /**
51
+ * Sets data by key (overwrites existing data).
52
+ */
53
+ set(key: string, data: Record<string, unknown>): Promise<boolean>;
54
+ /**
55
+ * Updates specific fields in data (merges with existing data).
56
+ */
57
+ update(key: string, updates: Record<string, unknown>): Promise<boolean>;
58
+ /**
59
+ * Deletes data by key.
60
+ */
61
+ delete(key: string): Promise<boolean>;
62
+ /**
63
+ * Closes the database connection (for MongoDB).
64
+ */
65
+ close(): Promise<void>;
66
+ }