@minesa-org/mini-interaction 0.1.3 → 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
+ }
@@ -1,8 +1,8 @@
1
1
  import type { MiniDataBuilder } from "./MiniDataBuilder.js";
2
2
  import type { DatabaseConfig } from "./MiniDatabaseBuilder.js";
3
3
  /**
4
- * MiniDatabase provides async data storage with support for JSON and MongoDB backends.
5
- * Designed to work seamlessly with Vercel and serverless environments.
4
+ * MiniDatabase provides async data storage backed by MongoDB.
5
+ * Designed to work seamlessly with Vercel and other serverless environments.
6
6
  *
7
7
  * @example
8
8
  * ```typescript
@@ -25,6 +25,15 @@ export declare class MiniDatabase {
25
25
  private mongoDb?;
26
26
  private mongoCollection?;
27
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;
28
37
  constructor(config: DatabaseConfig, schema?: MiniDataBuilder);
29
38
  /**
30
39
  * Initializes the database connection.
@@ -34,10 +43,6 @@ export declare class MiniDatabase {
34
43
  * Initializes MongoDB connection.
35
44
  */
36
45
  private initializeMongoDB;
37
- /**
38
- * Initializes JSON file storage.
39
- */
40
- private initializeJSON;
41
46
  /**
42
47
  * Gets data by key.
43
48
  */
@@ -1,8 +1,7 @@
1
- import { promises as fs } from "fs";
2
- import path from "path";
1
+ import { MiniDatabaseBuilder } from "./MiniDatabaseBuilder.js";
3
2
  /**
4
- * MiniDatabase provides async data storage with support for JSON and MongoDB backends.
5
- * Designed to work seamlessly with Vercel and serverless environments.
3
+ * MiniDatabase provides async data storage backed by MongoDB.
4
+ * Designed to work seamlessly with Vercel and other serverless environments.
6
5
  *
7
6
  * @example
8
7
  * ```typescript
@@ -25,6 +24,22 @@ export class MiniDatabase {
25
24
  mongoDb;
26
25
  mongoCollection;
27
26
  initPromise;
27
+ /**
28
+ * Creates a MiniDatabase instance using an environment variable for the MongoDB URI.
29
+ * Defaults to the `MONGODB_URI` variable expected by Vercel and many hosting providers.
30
+ */
31
+ static fromEnv(schema, options) {
32
+ const variable = options?.variable ?? "MONGODB_URI";
33
+ const uri = process.env[variable];
34
+ if (!uri) {
35
+ throw new Error(`[MiniDatabase] Environment variable "${variable}" is not set`);
36
+ }
37
+ const builder = new MiniDatabaseBuilder()
38
+ .setMongoUri(uri)
39
+ .setDbName(options?.dbName ?? "minidb")
40
+ .setCollectionName(options?.collectionName ?? "data");
41
+ return new MiniDatabase(builder.build(), schema);
42
+ }
28
43
  constructor(config, schema) {
29
44
  this.config = config;
30
45
  this.schema = schema;
@@ -36,14 +51,7 @@ export class MiniDatabase {
36
51
  if (this.initPromise) {
37
52
  return this.initPromise;
38
53
  }
39
- this.initPromise = (async () => {
40
- if (this.config.type === "mongodb") {
41
- await this.initializeMongoDB();
42
- }
43
- else {
44
- await this.initializeJSON();
45
- }
46
- })();
54
+ this.initPromise = this.initializeMongoDB();
47
55
  return this.initPromise;
48
56
  }
49
57
  /**
@@ -76,43 +84,22 @@ export class MiniDatabase {
76
84
  throw err;
77
85
  }
78
86
  }
79
- /**
80
- * Initializes JSON file storage.
81
- */
82
- async initializeJSON() {
83
- try {
84
- const dataPath = this.config.dataPath || "./data";
85
- await fs.mkdir(dataPath, { recursive: true });
86
- console.log("✅ [MiniDatabase] JSON storage initialized at", dataPath);
87
- }
88
- catch (err) {
89
- console.error("❌ [MiniDatabase] Failed to initialize JSON storage:", err);
90
- throw err;
91
- }
92
- }
93
87
  /**
94
88
  * Gets data by key.
95
89
  */
96
90
  async get(key) {
97
91
  await this.initialize();
98
92
  try {
99
- if (this.config.type === "mongodb") {
100
- const doc = await this.mongoCollection.findOne({ _id: key });
101
- return doc ? { ...doc, _id: undefined } : null;
93
+ const collection = this.mongoCollection;
94
+ if (!collection) {
95
+ throw new Error("MongoDB collection is not initialized");
102
96
  }
103
- else {
104
- const filePath = path.join(this.config.dataPath || "./data", `${key}.json`);
105
- try {
106
- const data = await fs.readFile(filePath, "utf-8");
107
- return JSON.parse(data);
108
- }
109
- catch (err) {
110
- if (err.code === "ENOENT") {
111
- return null;
112
- }
113
- throw err;
114
- }
97
+ const doc = await collection.findOne({ _id: key });
98
+ if (!doc) {
99
+ return null;
115
100
  }
101
+ const { _id, ...rest } = doc;
102
+ return rest;
116
103
  }
117
104
  catch (err) {
118
105
  console.error(`❌ [MiniDatabase] Failed to get data for key "${key}":`, err);
@@ -125,6 +112,10 @@ export class MiniDatabase {
125
112
  async set(key, data) {
126
113
  await this.initialize();
127
114
  try {
115
+ const collection = this.mongoCollection;
116
+ if (!collection) {
117
+ throw new Error("MongoDB collection is not initialized");
118
+ }
128
119
  // Validate against schema if provided
129
120
  if (this.schema) {
130
121
  const validation = this.schema.validate(data);
@@ -135,22 +126,16 @@ export class MiniDatabase {
135
126
  const dataWithDefaults = this.schema
136
127
  ? this.schema.applyDefaults(data)
137
128
  : data;
138
- if (this.config.type === "mongodb") {
139
- await this.mongoCollection.updateOne({ _id: key }, {
140
- $set: {
141
- ...dataWithDefaults,
142
- _id: key,
143
- updatedAt: new Date(),
144
- },
145
- $setOnInsert: {
146
- createdAt: new Date(),
147
- },
148
- }, { upsert: true });
149
- }
150
- else {
151
- const filePath = path.join(this.config.dataPath || "./data", `${key}.json`);
152
- await fs.writeFile(filePath, JSON.stringify(dataWithDefaults, null, 2));
153
- }
129
+ await collection.updateOne({ _id: key }, {
130
+ $set: {
131
+ ...dataWithDefaults,
132
+ _id: key,
133
+ updatedAt: new Date(),
134
+ },
135
+ $setOnInsert: {
136
+ createdAt: new Date(),
137
+ },
138
+ }, { upsert: true });
154
139
  console.log(`✅ [MiniDatabase] Saved data for key "${key}"`);
155
140
  return true;
156
141
  }
@@ -165,8 +150,12 @@ export class MiniDatabase {
165
150
  async update(key, updates) {
166
151
  await this.initialize();
167
152
  try {
153
+ const collection = this.mongoCollection;
154
+ if (!collection) {
155
+ throw new Error("MongoDB collection is not initialized");
156
+ }
168
157
  const existing = await this.get(key);
169
- const merged = { ...existing, ...updates };
158
+ const merged = { ...(existing ?? {}), ...updates };
170
159
  // Validate merged data against schema if provided
171
160
  if (this.schema) {
172
161
  const validation = this.schema.validate(merged);
@@ -177,21 +166,15 @@ export class MiniDatabase {
177
166
  const dataWithDefaults = this.schema
178
167
  ? this.schema.applyDefaults(merged)
179
168
  : merged;
180
- if (this.config.type === "mongodb") {
181
- await this.mongoCollection.updateOne({ _id: key }, {
182
- $set: {
183
- ...dataWithDefaults,
184
- updatedAt: new Date(),
185
- },
186
- $setOnInsert: {
187
- createdAt: new Date(),
188
- },
189
- }, { upsert: true });
190
- }
191
- else {
192
- const filePath = path.join(this.config.dataPath || "./data", `${key}.json`);
193
- await fs.writeFile(filePath, JSON.stringify(dataWithDefaults, null, 2));
194
- }
169
+ await collection.updateOne({ _id: key }, {
170
+ $set: {
171
+ ...dataWithDefaults,
172
+ updatedAt: new Date(),
173
+ },
174
+ $setOnInsert: {
175
+ createdAt: new Date(),
176
+ },
177
+ }, { upsert: true });
195
178
  console.log(`✅ [MiniDatabase] Updated data for key "${key}"`);
196
179
  return true;
197
180
  }
@@ -206,13 +189,11 @@ export class MiniDatabase {
206
189
  async delete(key) {
207
190
  await this.initialize();
208
191
  try {
209
- if (this.config.type === "mongodb") {
210
- await this.mongoCollection.deleteOne({ _id: key });
211
- }
212
- else {
213
- const filePath = path.join(this.config.dataPath || "./data", `${key}.json`);
214
- await fs.unlink(filePath);
192
+ const collection = this.mongoCollection;
193
+ if (!collection) {
194
+ throw new Error("MongoDB collection is not initialized");
215
195
  }
196
+ await collection.deleteOne({ _id: key });
216
197
  console.log(`✅ [MiniDatabase] Deleted data for key "${key}"`);
217
198
  return true;
218
199
  }
@@ -2,11 +2,9 @@
2
2
  * Configuration for MiniDatabase backend.
3
3
  */
4
4
  export interface DatabaseConfig {
5
- type: "json" | "mongodb";
6
- dataPath?: string;
7
- mongoUri?: string;
8
- dbName?: string;
9
- collectionName?: string;
5
+ mongoUri: string;
6
+ dbName: string;
7
+ collectionName: string;
10
8
  }
11
9
  /**
12
10
  * Builder for configuring MiniDatabase.
@@ -14,16 +12,8 @@ export interface DatabaseConfig {
14
12
  *
15
13
  * @example
16
14
  * ```typescript
17
- * // Using JSON (default)
18
15
  * const db = new MiniDatabaseBuilder()
19
- * .setType("json")
20
- * .setDataPath("./data")
21
- * .build();
22
- *
23
- * // Using MongoDB
24
- * const db = new MiniDatabaseBuilder()
25
- * .setType("mongodb")
26
- * .setMongoUri(process.env.MONGO_URI)
16
+ * .useMongoUriFromEnv()
27
17
  * .setDbName("myapp")
28
18
  * .setCollectionName("users")
29
19
  * .build();
@@ -31,19 +21,15 @@ export interface DatabaseConfig {
31
21
  */
32
22
  export declare class MiniDatabaseBuilder {
33
23
  private config;
34
- /**
35
- * Sets the database type (json or mongodb).
36
- */
37
- setType(type: "json" | "mongodb"): this;
38
- /**
39
- * Sets the data path for JSON backend.
40
- * Default: "./data"
41
- */
42
- setDataPath(path: string): this;
43
24
  /**
44
25
  * Sets the MongoDB connection URI.
45
26
  */
46
27
  setMongoUri(uri: string): this;
28
+ /**
29
+ * Reads the MongoDB connection URI from an environment variable.
30
+ * Defaults to `MONGODB_URI`.
31
+ */
32
+ useMongoUriFromEnv(variable?: string): this;
47
33
  /**
48
34
  * Sets the MongoDB database name.
49
35
  * Default: "minidb"
@@ -4,16 +4,8 @@
4
4
  *
5
5
  * @example
6
6
  * ```typescript
7
- * // Using JSON (default)
8
7
  * const db = new MiniDatabaseBuilder()
9
- * .setType("json")
10
- * .setDataPath("./data")
11
- * .build();
12
- *
13
- * // Using MongoDB
14
- * const db = new MiniDatabaseBuilder()
15
- * .setType("mongodb")
16
- * .setMongoUri(process.env.MONGO_URI)
8
+ * .useMongoUriFromEnv()
17
9
  * .setDbName("myapp")
18
10
  * .setCollectionName("users")
19
11
  * .build();
@@ -21,31 +13,27 @@
21
13
  */
22
14
  export class MiniDatabaseBuilder {
23
15
  config = {
24
- type: "json",
25
- dataPath: "./data",
16
+ mongoUri: process.env.MONGODB_URI ?? "",
26
17
  dbName: "minidb",
27
18
  collectionName: "data",
28
19
  };
29
20
  /**
30
- * Sets the database type (json or mongodb).
31
- */
32
- setType(type) {
33
- this.config.type = type;
34
- return this;
35
- }
36
- /**
37
- * Sets the data path for JSON backend.
38
- * Default: "./data"
21
+ * Sets the MongoDB connection URI.
39
22
  */
40
- setDataPath(path) {
41
- this.config.dataPath = path;
23
+ setMongoUri(uri) {
24
+ this.config.mongoUri = uri;
42
25
  return this;
43
26
  }
44
27
  /**
45
- * Sets the MongoDB connection URI.
28
+ * Reads the MongoDB connection URI from an environment variable.
29
+ * Defaults to `MONGODB_URI`.
46
30
  */
47
- setMongoUri(uri) {
48
- this.config.mongoUri = uri;
31
+ useMongoUriFromEnv(variable = "MONGODB_URI") {
32
+ const value = process.env[variable];
33
+ if (!value) {
34
+ throw new Error(`[MiniDatabaseBuilder] Environment variable "${variable}" is not set`);
35
+ }
36
+ this.config.mongoUri = value;
49
37
  return this;
50
38
  }
51
39
  /**
@@ -75,15 +63,8 @@ export class MiniDatabaseBuilder {
75
63
  */
76
64
  validate() {
77
65
  const errors = [];
78
- if (this.config.type === "mongodb") {
79
- if (!this.config.mongoUri) {
80
- errors.push("MongoDB URI is required when using mongodb backend");
81
- }
82
- }
83
- if (this.config.type === "json") {
84
- if (!this.config.dataPath) {
85
- errors.push("Data path is required when using json backend");
86
- }
66
+ if (!this.config.mongoUri) {
67
+ errors.push("MongoDB URI is required");
87
68
  }
88
69
  return {
89
70
  valid: errors.length === 0,
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export type { AttachmentOptionBuilder, ChannelOptionBuilder, MentionableOptionBu
7
7
  export { CommandInteractionOptionResolver, createCommandInteraction, } from "./utils/CommandInteractionOptions.js";
8
8
  export type { CommandInteraction, MentionableOption, ResolvedUserOption, } from "./utils/CommandInteractionOptions.js";
9
9
  export type { UserContextMenuInteraction, MessageContextMenuInteraction, } from "./utils/ContextMenuInteraction.js";
10
- export type { MiniInteractionFetchHandler, MiniInteractionNodeHandler, MiniInteractionHandlerResult, MiniInteractionRequest, MiniInteractionOptions, } from "./clients/MiniInteraction.js";
10
+ export type { MiniInteractionFetchHandler, MiniInteractionNodeHandler, MiniInteractionHandlerResult, MiniInteractionRequest, MiniInteractionOptions, DiscordOAuthAuthorizeContext, DiscordOAuthCallbackOptions, DiscordOAuthCallbackTemplates, DiscordOAuthErrorTemplateContext, DiscordOAuthServerErrorTemplateContext, DiscordOAuthStateTemplateContext, DiscordOAuthSuccessTemplateContext, } from "./clients/MiniInteraction.js";
11
11
  export type { MiniInteractionCommand, SlashCommandHandler, UserCommandHandler, MessageCommandHandler, CommandHandler, } from "./types/Commands.js";
12
12
  export type { MiniInteractionComponent, MiniInteractionButtonHandler, MiniInteractionStringSelectHandler, MiniInteractionRoleSelectHandler, MiniInteractionUserSelectHandler, MiniInteractionChannelSelectHandler, MiniInteractionMentionableSelectHandler, MiniInteractionComponentHandler, MiniInteractionModal, MiniInteractionModalHandler, MiniInteractionHandler, } from "./clients/MiniInteraction.js";
13
13
  export type { MessageComponentInteraction, ButtonInteraction, StringSelectInteraction, RoleSelectInteraction, UserSelectInteraction, ChannelSelectInteraction, MentionableSelectInteraction, ResolvedUserOption as ComponentResolvedUserOption, ResolvedMentionableOption as ComponentResolvedMentionableOption, } from "./utils/MessageComponentInteraction.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Mini interaction, connecting your app with Discord via HTTP-interaction (Vercel support).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",