@minesa-org/mini-interaction 0.1.1 → 0.1.3

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.
@@ -1,5 +1,6 @@
1
1
  import { type APISelectMenuOption, type APIStringSelectComponent } from "discord-api-types/v10";
2
2
  import type { JSONEncodable } from "./shared.js";
3
+ import type { StringSelectMenuOptionBuilder } from "./StringSelectMenuOptionBuilder.js";
3
4
  /** Shape describing initial modal string select menu data accepted by the builder. */
4
5
  export type ModalStringSelectMenuBuilderData = {
5
6
  customId?: string;
@@ -44,11 +45,15 @@ export declare class ModalStringSelectMenuBuilder implements JSONEncodable<APISt
44
45
  /**
45
46
  * Adds an option to the select menu.
46
47
  */
47
- addOption(option: APISelectMenuOption): this;
48
+ addOption(option: APISelectMenuOption | StringSelectMenuOptionBuilder | JSONEncodable<APISelectMenuOption>): this;
49
+ /**
50
+ * Appends new option entries to the select menu.
51
+ */
52
+ addOptions(...options: (APISelectMenuOption | StringSelectMenuOptionBuilder | JSONEncodable<APISelectMenuOption>)[]): this;
48
53
  /**
49
54
  * Replaces all options in the select menu.
50
55
  */
51
- setOptions(options: Iterable<APISelectMenuOption>): this;
56
+ setOptions(options: Iterable<APISelectMenuOption | StringSelectMenuOptionBuilder | JSONEncodable<APISelectMenuOption>>): this;
52
57
  /**
53
58
  * Serialises the builder into an API compatible string select menu payload.
54
59
  */
@@ -62,14 +62,36 @@ export class ModalStringSelectMenuBuilder {
62
62
  * Adds an option to the select menu.
63
63
  */
64
64
  addOption(option) {
65
- this.data.options.push(option);
65
+ if ("toJSON" in option && typeof option.toJSON === "function") {
66
+ this.data.options.push({ ...option.toJSON() });
67
+ }
68
+ else {
69
+ this.data.options.push({ ...option });
70
+ }
71
+ return this;
72
+ }
73
+ /**
74
+ * Appends new option entries to the select menu.
75
+ */
76
+ addOptions(...options) {
77
+ this.data.options.push(...options.map((option) => {
78
+ if ("toJSON" in option && typeof option.toJSON === "function") {
79
+ return { ...option.toJSON() };
80
+ }
81
+ return { ...option };
82
+ }));
66
83
  return this;
67
84
  }
68
85
  /**
69
86
  * Replaces all options in the select menu.
70
87
  */
71
88
  setOptions(options) {
72
- this.data.options = Array.from(options);
89
+ this.data.options = Array.from(options, (option) => {
90
+ if ("toJSON" in option && typeof option.toJSON === "function") {
91
+ return { ...option.toJSON() };
92
+ }
93
+ return { ...option };
94
+ });
73
95
  return this;
74
96
  }
75
97
  /**
@@ -1,5 +1,6 @@
1
1
  import { type APISelectMenuOption, type APIStringSelectComponent } from "discord-api-types/v10";
2
2
  import type { JSONEncodable } from "./shared.js";
3
+ import type { StringSelectMenuOptionBuilder } from "./StringSelectMenuOptionBuilder.js";
3
4
  /** Shape describing initial string select menu data accepted by the builder. */
4
5
  export type StringSelectMenuBuilderData = {
5
6
  customId?: string;
@@ -39,11 +40,11 @@ export declare class StringSelectMenuBuilder implements JSONEncodable<APIStringS
39
40
  /**
40
41
  * Appends new option entries to the select menu.
41
42
  */
42
- addOptions(...options: APISelectMenuOption[]): this;
43
+ addOptions(...options: (APISelectMenuOption | StringSelectMenuOptionBuilder | JSONEncodable<APISelectMenuOption>)[]): this;
43
44
  /**
44
45
  * Replaces the current option set with the provided iterable.
45
46
  */
46
- setOptions(options: Iterable<APISelectMenuOption>): this;
47
+ setOptions(options: Iterable<APISelectMenuOption | StringSelectMenuOptionBuilder | JSONEncodable<APISelectMenuOption>>): this;
47
48
  /**
48
49
  * Serialises the builder into an API compatible string select menu payload.
49
50
  */
@@ -54,14 +54,24 @@ export class StringSelectMenuBuilder {
54
54
  * Appends new option entries to the select menu.
55
55
  */
56
56
  addOptions(...options) {
57
- this.data.options.push(...options.map((option) => ({ ...option })));
57
+ this.data.options.push(...options.map((option) => {
58
+ if ("toJSON" in option && typeof option.toJSON === "function") {
59
+ return { ...option.toJSON() };
60
+ }
61
+ return { ...option };
62
+ }));
58
63
  return this;
59
64
  }
60
65
  /**
61
66
  * Replaces the current option set with the provided iterable.
62
67
  */
63
68
  setOptions(options) {
64
- this.data.options = Array.from(options, (option) => ({ ...option }));
69
+ this.data.options = Array.from(options, (option) => {
70
+ if ("toJSON" in option && typeof option.toJSON === "function") {
71
+ return { ...option.toJSON() };
72
+ }
73
+ return { ...option };
74
+ });
65
75
  return this;
66
76
  }
67
77
  /**
@@ -0,0 +1,46 @@
1
+ import type { APIMessageComponentEmoji, APISelectMenuOption } from "discord-api-types/v10";
2
+ import type { JSONEncodable } from "./shared.js";
3
+ /** Shape describing initial string select menu option data accepted by the builder. */
4
+ export type StringSelectMenuOptionBuilderData = {
5
+ label?: string;
6
+ value?: string;
7
+ description?: string;
8
+ emoji?: string | APIMessageComponentEmoji;
9
+ default?: boolean;
10
+ };
11
+ /** Builder for Discord string select menu option components. */
12
+ export declare class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMenuOption> {
13
+ private data;
14
+ /**
15
+ * Creates a new string select menu option builder with optional seed data.
16
+ */
17
+ constructor(data?: StringSelectMenuOptionBuilderData);
18
+ /**
19
+ * Sets the user-facing name of the option (max 100 characters).
20
+ */
21
+ setLabel(label: string): this;
22
+ /**
23
+ * Sets the dev-defined value of the option (max 100 characters).
24
+ */
25
+ setValue(value: string): this;
26
+ /**
27
+ * Sets an additional description of the option (max 100 characters).
28
+ */
29
+ setDescription(description: string | null | undefined): this;
30
+ /**
31
+ * Sets the emoji to display to the left of the option.
32
+ * Accepts:
33
+ * - Unicode emoji: "😀"
34
+ * - Custom emoji string: "<:name:id>" or "<a:name:id>"
35
+ * - Emoji object: { name: "😀" } or { name: "name", id: "id", animated: true }
36
+ */
37
+ setEmoji(emoji: string | APIMessageComponentEmoji | null | undefined): this;
38
+ /**
39
+ * Sets whether this option should be already-selected by default.
40
+ */
41
+ setDefault(isDefault: boolean): this;
42
+ /**
43
+ * Serialises the builder into an API compatible select menu option payload.
44
+ */
45
+ toJSON(): APISelectMenuOption;
46
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Parses an emoji string into an APIMessageComponentEmoji object.
3
+ * Supports:
4
+ * - Unicode emojis: "😀"
5
+ * - Custom emojis: "<:name:id>" or "<a:name:id>"
6
+ * - Emoji objects: { name: "😀" } or { name: "name", id: "id" }
7
+ */
8
+ function parseEmoji(emoji) {
9
+ if (typeof emoji === "object") {
10
+ return emoji;
11
+ }
12
+ // Check for custom emoji format: <:name:id> or <a:name:id>
13
+ const customEmojiMatch = emoji.match(/^<(a)?:([^:]+):(\d+)>$/);
14
+ if (customEmojiMatch) {
15
+ return {
16
+ name: customEmojiMatch[2],
17
+ id: customEmojiMatch[3],
18
+ animated: customEmojiMatch[1] === "a",
19
+ };
20
+ }
21
+ // Treat as unicode emoji
22
+ return { name: emoji };
23
+ }
24
+ /** Builder for Discord string select menu option components. */
25
+ export class StringSelectMenuOptionBuilder {
26
+ data;
27
+ /**
28
+ * Creates a new string select menu option builder with optional seed data.
29
+ */
30
+ constructor(data = {}) {
31
+ this.data = {
32
+ label: data.label,
33
+ value: data.value,
34
+ description: data.description,
35
+ emoji: data.emoji ? parseEmoji(data.emoji) : undefined,
36
+ default: data.default,
37
+ };
38
+ }
39
+ /**
40
+ * Sets the user-facing name of the option (max 100 characters).
41
+ */
42
+ setLabel(label) {
43
+ this.data.label = label;
44
+ return this;
45
+ }
46
+ /**
47
+ * Sets the dev-defined value of the option (max 100 characters).
48
+ */
49
+ setValue(value) {
50
+ this.data.value = value;
51
+ return this;
52
+ }
53
+ /**
54
+ * Sets an additional description of the option (max 100 characters).
55
+ */
56
+ setDescription(description) {
57
+ this.data.description = description ?? undefined;
58
+ return this;
59
+ }
60
+ /**
61
+ * Sets the emoji to display to the left of the option.
62
+ * Accepts:
63
+ * - Unicode emoji: "😀"
64
+ * - Custom emoji string: "<:name:id>" or "<a:name:id>"
65
+ * - Emoji object: { name: "😀" } or { name: "name", id: "id", animated: true }
66
+ */
67
+ setEmoji(emoji) {
68
+ this.data.emoji = emoji ? parseEmoji(emoji) : undefined;
69
+ return this;
70
+ }
71
+ /**
72
+ * Sets whether this option should be already-selected by default.
73
+ */
74
+ setDefault(isDefault) {
75
+ this.data.default = isDefault;
76
+ return this;
77
+ }
78
+ /**
79
+ * Serialises the builder into an API compatible select menu option payload.
80
+ */
81
+ toJSON() {
82
+ const { label, value } = this.data;
83
+ if (!label) {
84
+ throw new Error("[StringSelectMenuOptionBuilder] label is required.");
85
+ }
86
+ if (!value) {
87
+ throw new Error("[StringSelectMenuOptionBuilder] value is required.");
88
+ }
89
+ if (label.length > 100) {
90
+ throw new Error("[StringSelectMenuOptionBuilder] label must be 100 characters or less.");
91
+ }
92
+ if (value.length > 100) {
93
+ throw new Error("[StringSelectMenuOptionBuilder] value must be 100 characters or less.");
94
+ }
95
+ if (this.data.description && this.data.description.length > 100) {
96
+ throw new Error("[StringSelectMenuOptionBuilder] description must be 100 characters or less.");
97
+ }
98
+ return {
99
+ label,
100
+ value,
101
+ description: this.data.description,
102
+ emoji: this.data.emoji ? { ...this.data.emoji } : undefined,
103
+ default: this.data.default,
104
+ };
105
+ }
106
+ }
@@ -5,6 +5,8 @@ export { ButtonBuilder } from "./ButtonBuilder.js";
5
5
  export type { ButtonBuilderData } from "./ButtonBuilder.js";
6
6
  export { StringSelectMenuBuilder } from "./StringSelectMenuBuilder.js";
7
7
  export type { StringSelectMenuBuilderData } from "./StringSelectMenuBuilder.js";
8
+ export { StringSelectMenuOptionBuilder } from "./StringSelectMenuOptionBuilder.js";
9
+ export type { StringSelectMenuOptionBuilderData } from "./StringSelectMenuOptionBuilder.js";
8
10
  export { RoleSelectMenuBuilder } from "./RoleSelectMenuBuilder.js";
9
11
  export type { RoleSelectMenuBuilderData } from "./RoleSelectMenuBuilder.js";
10
12
  export { ChannelSelectMenuBuilder } from "./ChannelSelectMenuBuilder.js";
@@ -2,6 +2,7 @@
2
2
  export { ActionRowBuilder } from "./ActionRowBuilder.js";
3
3
  export { ButtonBuilder } from "./ButtonBuilder.js";
4
4
  export { StringSelectMenuBuilder } from "./StringSelectMenuBuilder.js";
5
+ export { StringSelectMenuOptionBuilder } from "./StringSelectMenuOptionBuilder.js";
5
6
  export { RoleSelectMenuBuilder } from "./RoleSelectMenuBuilder.js";
6
7
  export { ChannelSelectMenuBuilder } from "./ChannelSelectMenuBuilder.js";
7
8
  export { ModalStringSelectMenuBuilder } from "./ModalStringSelectMenuBuilder.js";
@@ -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,61 @@
1
+ import type { MiniDataBuilder } from "./MiniDataBuilder.js";
2
+ import type { DatabaseConfig } from "./MiniDatabaseBuilder.js";
3
+ /**
4
+ * MiniDatabase provides async data storage with support for JSON and MongoDB backends.
5
+ * Designed to work seamlessly with Vercel and 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
+ constructor(config: DatabaseConfig, schema?: MiniDataBuilder);
29
+ /**
30
+ * Initializes the database connection.
31
+ */
32
+ private initialize;
33
+ /**
34
+ * Initializes MongoDB connection.
35
+ */
36
+ private initializeMongoDB;
37
+ /**
38
+ * Initializes JSON file storage.
39
+ */
40
+ private initializeJSON;
41
+ /**
42
+ * Gets data by key.
43
+ */
44
+ get(key: string): Promise<Record<string, unknown> | null>;
45
+ /**
46
+ * Sets data by key (overwrites existing data).
47
+ */
48
+ set(key: string, data: Record<string, unknown>): Promise<boolean>;
49
+ /**
50
+ * Updates specific fields in data (merges with existing data).
51
+ */
52
+ update(key: string, updates: Record<string, unknown>): Promise<boolean>;
53
+ /**
54
+ * Deletes data by key.
55
+ */
56
+ delete(key: string): Promise<boolean>;
57
+ /**
58
+ * Closes the database connection (for MongoDB).
59
+ */
60
+ close(): Promise<void>;
61
+ }
@@ -0,0 +1,233 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ /**
4
+ * MiniDatabase provides async data storage with support for JSON and MongoDB backends.
5
+ * Designed to work seamlessly with Vercel and 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 class MiniDatabase {
22
+ config;
23
+ schema;
24
+ mongoClient;
25
+ mongoDb;
26
+ mongoCollection;
27
+ initPromise;
28
+ constructor(config, schema) {
29
+ this.config = config;
30
+ this.schema = schema;
31
+ }
32
+ /**
33
+ * Initializes the database connection.
34
+ */
35
+ async initialize() {
36
+ if (this.initPromise) {
37
+ return this.initPromise;
38
+ }
39
+ this.initPromise = (async () => {
40
+ if (this.config.type === "mongodb") {
41
+ await this.initializeMongoDB();
42
+ }
43
+ else {
44
+ await this.initializeJSON();
45
+ }
46
+ })();
47
+ return this.initPromise;
48
+ }
49
+ /**
50
+ * Initializes MongoDB connection.
51
+ */
52
+ async initializeMongoDB() {
53
+ try {
54
+ let MongoClient;
55
+ try {
56
+ // @ts-ignore - MongoDB is optional
57
+ const mongoModule = await import("mongodb");
58
+ MongoClient = mongoModule.MongoClient;
59
+ }
60
+ catch {
61
+ throw new Error("MongoDB driver not installed. Install it with: npm install mongodb");
62
+ }
63
+ if (!this.config.mongoUri) {
64
+ throw new Error("MongoDB URI is required");
65
+ }
66
+ this.mongoClient = new MongoClient(this.config.mongoUri, {
67
+ maxPoolSize: 5,
68
+ });
69
+ await this.mongoClient.connect();
70
+ this.mongoDb = this.mongoClient.db(this.config.dbName || "minidb");
71
+ this.mongoCollection = this.mongoDb.collection(this.config.collectionName || "data");
72
+ console.log("✅ [MiniDatabase] Connected to MongoDB");
73
+ }
74
+ catch (err) {
75
+ console.error("❌ [MiniDatabase] Failed to connect to MongoDB:", err);
76
+ throw err;
77
+ }
78
+ }
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
+ /**
94
+ * Gets data by key.
95
+ */
96
+ async get(key) {
97
+ await this.initialize();
98
+ try {
99
+ if (this.config.type === "mongodb") {
100
+ const doc = await this.mongoCollection.findOne({ _id: key });
101
+ return doc ? { ...doc, _id: undefined } : null;
102
+ }
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
+ }
115
+ }
116
+ }
117
+ catch (err) {
118
+ console.error(`❌ [MiniDatabase] Failed to get data for key "${key}":`, err);
119
+ return null;
120
+ }
121
+ }
122
+ /**
123
+ * Sets data by key (overwrites existing data).
124
+ */
125
+ async set(key, data) {
126
+ await this.initialize();
127
+ try {
128
+ // Validate against schema if provided
129
+ if (this.schema) {
130
+ const validation = this.schema.validate(data);
131
+ if (!validation.valid) {
132
+ throw new Error(`Validation failed: ${validation.errors.join(", ")}`);
133
+ }
134
+ }
135
+ const dataWithDefaults = this.schema
136
+ ? this.schema.applyDefaults(data)
137
+ : 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
+ }
154
+ console.log(`✅ [MiniDatabase] Saved data for key "${key}"`);
155
+ return true;
156
+ }
157
+ catch (err) {
158
+ console.error(`❌ [MiniDatabase] Failed to set data for key "${key}":`, err);
159
+ return false;
160
+ }
161
+ }
162
+ /**
163
+ * Updates specific fields in data (merges with existing data).
164
+ */
165
+ async update(key, updates) {
166
+ await this.initialize();
167
+ try {
168
+ const existing = await this.get(key);
169
+ const merged = { ...existing, ...updates };
170
+ // Validate merged data against schema if provided
171
+ if (this.schema) {
172
+ const validation = this.schema.validate(merged);
173
+ if (!validation.valid) {
174
+ throw new Error(`Validation failed: ${validation.errors.join(", ")}`);
175
+ }
176
+ }
177
+ const dataWithDefaults = this.schema
178
+ ? this.schema.applyDefaults(merged)
179
+ : 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
+ }
195
+ console.log(`✅ [MiniDatabase] Updated data for key "${key}"`);
196
+ return true;
197
+ }
198
+ catch (err) {
199
+ console.error(`❌ [MiniDatabase] Failed to update data for key "${key}":`, err);
200
+ return false;
201
+ }
202
+ }
203
+ /**
204
+ * Deletes data by key.
205
+ */
206
+ async delete(key) {
207
+ await this.initialize();
208
+ 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);
215
+ }
216
+ console.log(`✅ [MiniDatabase] Deleted data for key "${key}"`);
217
+ return true;
218
+ }
219
+ catch (err) {
220
+ console.error(`❌ [MiniDatabase] Failed to delete data for key "${key}":`, err);
221
+ return false;
222
+ }
223
+ }
224
+ /**
225
+ * Closes the database connection (for MongoDB).
226
+ */
227
+ async close() {
228
+ if (this.mongoClient) {
229
+ await this.mongoClient.close();
230
+ console.log("✅ [MiniDatabase] MongoDB connection closed");
231
+ }
232
+ }
233
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Configuration for MiniDatabase backend.
3
+ */
4
+ export interface DatabaseConfig {
5
+ type: "json" | "mongodb";
6
+ dataPath?: string;
7
+ mongoUri?: string;
8
+ dbName?: string;
9
+ collectionName?: string;
10
+ }
11
+ /**
12
+ * Builder for configuring MiniDatabase.
13
+ * Provides a fluent API for easy database setup.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // Using JSON (default)
18
+ * 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)
27
+ * .setDbName("myapp")
28
+ * .setCollectionName("users")
29
+ * .build();
30
+ * ```
31
+ */
32
+ export declare class MiniDatabaseBuilder {
33
+ 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
+ /**
44
+ * Sets the MongoDB connection URI.
45
+ */
46
+ setMongoUri(uri: string): this;
47
+ /**
48
+ * Sets the MongoDB database name.
49
+ * Default: "minidb"
50
+ */
51
+ setDbName(name: string): this;
52
+ /**
53
+ * Sets the MongoDB collection name.
54
+ * Default: "data"
55
+ */
56
+ setCollectionName(name: string): this;
57
+ /**
58
+ * Gets the current configuration.
59
+ */
60
+ getConfig(): DatabaseConfig;
61
+ /**
62
+ * Validates the configuration.
63
+ */
64
+ validate(): {
65
+ valid: boolean;
66
+ errors: string[];
67
+ };
68
+ /**
69
+ * Builds and returns the configuration.
70
+ */
71
+ build(): DatabaseConfig;
72
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Builder for configuring MiniDatabase.
3
+ * Provides a fluent API for easy database setup.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * // Using JSON (default)
8
+ * 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)
17
+ * .setDbName("myapp")
18
+ * .setCollectionName("users")
19
+ * .build();
20
+ * ```
21
+ */
22
+ export class MiniDatabaseBuilder {
23
+ config = {
24
+ type: "json",
25
+ dataPath: "./data",
26
+ dbName: "minidb",
27
+ collectionName: "data",
28
+ };
29
+ /**
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"
39
+ */
40
+ setDataPath(path) {
41
+ this.config.dataPath = path;
42
+ return this;
43
+ }
44
+ /**
45
+ * Sets the MongoDB connection URI.
46
+ */
47
+ setMongoUri(uri) {
48
+ this.config.mongoUri = uri;
49
+ return this;
50
+ }
51
+ /**
52
+ * Sets the MongoDB database name.
53
+ * Default: "minidb"
54
+ */
55
+ setDbName(name) {
56
+ this.config.dbName = name;
57
+ return this;
58
+ }
59
+ /**
60
+ * Sets the MongoDB collection name.
61
+ * Default: "data"
62
+ */
63
+ setCollectionName(name) {
64
+ this.config.collectionName = name;
65
+ return this;
66
+ }
67
+ /**
68
+ * Gets the current configuration.
69
+ */
70
+ getConfig() {
71
+ return { ...this.config };
72
+ }
73
+ /**
74
+ * Validates the configuration.
75
+ */
76
+ validate() {
77
+ 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
+ }
87
+ }
88
+ return {
89
+ valid: errors.length === 0,
90
+ errors,
91
+ };
92
+ }
93
+ /**
94
+ * Builds and returns the configuration.
95
+ */
96
+ build() {
97
+ const validation = this.validate();
98
+ if (!validation.valid) {
99
+ throw new Error(`Invalid database configuration: ${validation.errors.join(", ")}`);
100
+ }
101
+ return this.getConfig();
102
+ }
103
+ }
package/dist/index.d.ts CHANGED
@@ -21,3 +21,11 @@ export { TextInputStyle } from "discord-api-types/v10";
21
21
  export { MiniPermFlags } from "./types/PermissionFlags.js";
22
22
  export type { MiniComponentActionRow, MiniComponentMessageActionRow, } from "./types/ComponentTypes.js";
23
23
  export * from "./builders/index.js";
24
+ export { MiniDataBuilder } from "./database/MiniDataBuilder.js";
25
+ export type { DataField } from "./database/MiniDataBuilder.js";
26
+ export { MiniDatabaseBuilder } from "./database/MiniDatabaseBuilder.js";
27
+ export type { DatabaseConfig } from "./database/MiniDatabaseBuilder.js";
28
+ export { MiniDatabase } from "./database/MiniDatabase.js";
29
+ export { generateOAuthUrl, getOAuthTokens, refreshAccessToken, getDiscordUser, ensureValidToken, } from "./oauth/DiscordOAuth.js";
30
+ export type { OAuthConfig, OAuthTokens, DiscordUser, } from "./oauth/DiscordOAuth.js";
31
+ export { OAuthTokenStorage } from "./oauth/OAuthTokenStorage.js";
package/dist/index.js CHANGED
@@ -11,3 +11,8 @@ export { SeparatorSpacingSize } from "./types/SeparatorSpacingSize.js";
11
11
  export { TextInputStyle } from "discord-api-types/v10";
12
12
  export { MiniPermFlags } from "./types/PermissionFlags.js";
13
13
  export * from "./builders/index.js";
14
+ export { MiniDataBuilder } from "./database/MiniDataBuilder.js";
15
+ export { MiniDatabaseBuilder } from "./database/MiniDatabaseBuilder.js";
16
+ export { MiniDatabase } from "./database/MiniDatabase.js";
17
+ export { generateOAuthUrl, getOAuthTokens, refreshAccessToken, getDiscordUser, ensureValidToken, } from "./oauth/DiscordOAuth.js";
18
+ export { OAuthTokenStorage } from "./oauth/OAuthTokenStorage.js";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Discord OAuth utilities for handling the OAuth2 flow.
3
+ * Simplifies Discord authentication for mini-interaction apps.
4
+ */
5
+ export interface OAuthConfig {
6
+ appId: string;
7
+ appSecret: string;
8
+ redirectUri: string;
9
+ }
10
+ export interface OAuthTokens {
11
+ access_token: string;
12
+ refresh_token: string;
13
+ expires_at: number;
14
+ token_type: string;
15
+ scope: string;
16
+ }
17
+ export interface DiscordUser {
18
+ id: string;
19
+ username: string;
20
+ discriminator: string;
21
+ avatar: string | null;
22
+ email?: string;
23
+ verified?: boolean;
24
+ locale?: string;
25
+ mfa_enabled?: boolean;
26
+ }
27
+ /**
28
+ * Generates an OAuth authorization URL for Discord.
29
+ */
30
+ export declare function generateOAuthUrl(config: OAuthConfig, scopes?: string[]): {
31
+ url: string;
32
+ state: string;
33
+ };
34
+ /**
35
+ * Exchanges an OAuth code for access tokens.
36
+ */
37
+ export declare function getOAuthTokens(code: string, config: OAuthConfig): Promise<OAuthTokens>;
38
+ /**
39
+ * Refreshes an expired access token.
40
+ */
41
+ export declare function refreshAccessToken(refreshToken: string, config: OAuthConfig): Promise<OAuthTokens>;
42
+ /**
43
+ * Gets the current user's Discord profile.
44
+ */
45
+ export declare function getDiscordUser(accessToken: string): Promise<DiscordUser>;
46
+ /**
47
+ * Ensures the access token is valid, refreshing if necessary.
48
+ */
49
+ export declare function ensureValidToken(tokens: OAuthTokens, config: OAuthConfig): Promise<OAuthTokens>;
@@ -0,0 +1,110 @@
1
+ import crypto from "crypto";
2
+ /**
3
+ * Generates an OAuth authorization URL for Discord.
4
+ */
5
+ export function generateOAuthUrl(config, scopes = ["identify", "email"]) {
6
+ const state = crypto.randomUUID();
7
+ const url = new URL("https://discord.com/api/oauth2/authorize");
8
+ url.searchParams.set("client_id", config.appId);
9
+ url.searchParams.set("redirect_uri", config.redirectUri);
10
+ url.searchParams.set("response_type", "code");
11
+ url.searchParams.set("state", state);
12
+ url.searchParams.set("scope", scopes.join(" "));
13
+ url.searchParams.set("prompt", "consent");
14
+ return { url: url.toString(), state };
15
+ }
16
+ /**
17
+ * Exchanges an OAuth code for access tokens.
18
+ */
19
+ export async function getOAuthTokens(code, config) {
20
+ const url = "https://discord.com/api/v10/oauth2/token";
21
+ const body = new URLSearchParams({
22
+ client_id: config.appId,
23
+ client_secret: config.appSecret,
24
+ grant_type: "authorization_code",
25
+ code,
26
+ redirect_uri: config.redirectUri,
27
+ });
28
+ const response = await fetch(url, {
29
+ method: "POST",
30
+ body,
31
+ headers: {
32
+ "Content-Type": "application/x-www-form-urlencoded",
33
+ },
34
+ });
35
+ if (!response.ok) {
36
+ throw new Error(`Failed to get OAuth tokens: [${response.status}] ${response.statusText}`);
37
+ }
38
+ const data = (await response.json());
39
+ return {
40
+ access_token: data.access_token,
41
+ refresh_token: data.refresh_token,
42
+ expires_at: Date.now() + data.expires_in * 1000,
43
+ token_type: data.token_type,
44
+ scope: data.scope,
45
+ };
46
+ }
47
+ /**
48
+ * Refreshes an expired access token.
49
+ */
50
+ export async function refreshAccessToken(refreshToken, config) {
51
+ const url = "https://discord.com/api/v10/oauth2/token";
52
+ const body = new URLSearchParams({
53
+ client_id: config.appId,
54
+ client_secret: config.appSecret,
55
+ grant_type: "refresh_token",
56
+ refresh_token: refreshToken,
57
+ });
58
+ const response = await fetch(url, {
59
+ method: "POST",
60
+ body,
61
+ headers: {
62
+ "Content-Type": "application/x-www-form-urlencoded",
63
+ },
64
+ });
65
+ if (!response.ok) {
66
+ throw new Error(`Failed to refresh access token: [${response.status}] ${response.statusText}`);
67
+ }
68
+ const data = (await response.json());
69
+ return {
70
+ access_token: data.access_token,
71
+ refresh_token: data.refresh_token,
72
+ expires_at: Date.now() + data.expires_in * 1000,
73
+ token_type: data.token_type,
74
+ scope: data.scope,
75
+ };
76
+ }
77
+ /**
78
+ * Gets the current user's Discord profile.
79
+ */
80
+ export async function getDiscordUser(accessToken) {
81
+ const url = "https://discord.com/api/v10/oauth2/@me";
82
+ const response = await fetch(url, {
83
+ headers: {
84
+ Authorization: `Bearer ${accessToken}`,
85
+ },
86
+ });
87
+ if (!response.ok) {
88
+ throw new Error(`Failed to get Discord user: [${response.status}] ${response.statusText}`);
89
+ }
90
+ const data = (await response.json());
91
+ return {
92
+ id: data.user.id,
93
+ username: data.user.username,
94
+ discriminator: data.user.discriminator,
95
+ avatar: data.user.avatar,
96
+ email: data.user.email,
97
+ verified: data.user.verified,
98
+ locale: data.user.locale,
99
+ mfa_enabled: data.user.mfa_enabled,
100
+ };
101
+ }
102
+ /**
103
+ * Ensures the access token is valid, refreshing if necessary.
104
+ */
105
+ export async function ensureValidToken(tokens, config) {
106
+ if (Date.now() > tokens.expires_at) {
107
+ return refreshAccessToken(tokens.refresh_token, config);
108
+ }
109
+ return tokens;
110
+ }
@@ -0,0 +1,31 @@
1
+ import type { MiniDatabase } from "../database/MiniDatabase.js";
2
+ import type { OAuthTokens } from "./DiscordOAuth.js";
3
+ /**
4
+ * Manages OAuth token storage using MiniDatabase.
5
+ * Handles both in-memory and persistent storage.
6
+ */
7
+ export declare class OAuthTokenStorage {
8
+ private db?;
9
+ private inMemoryStore;
10
+ constructor(db?: MiniDatabase);
11
+ /**
12
+ * Stores OAuth tokens for a user.
13
+ */
14
+ storeTokens(userId: string, tokens: OAuthTokens): Promise<boolean>;
15
+ /**
16
+ * Retrieves OAuth tokens for a user.
17
+ */
18
+ getTokens(userId: string): Promise<OAuthTokens | null>;
19
+ /**
20
+ * Updates OAuth tokens for a user.
21
+ */
22
+ updateTokens(userId: string, tokens: OAuthTokens): Promise<boolean>;
23
+ /**
24
+ * Deletes OAuth tokens for a user.
25
+ */
26
+ deleteTokens(userId: string): Promise<boolean>;
27
+ /**
28
+ * Clears all in-memory tokens (useful for testing).
29
+ */
30
+ clearMemory(): void;
31
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Manages OAuth token storage using MiniDatabase.
3
+ * Handles both in-memory and persistent storage.
4
+ */
5
+ export class OAuthTokenStorage {
6
+ db;
7
+ inMemoryStore = new Map();
8
+ constructor(db) {
9
+ this.db = db;
10
+ }
11
+ /**
12
+ * Stores OAuth tokens for a user.
13
+ */
14
+ async storeTokens(userId, tokens) {
15
+ try {
16
+ // Always store in memory for fast access
17
+ this.inMemoryStore.set(`oauth-${userId}`, tokens);
18
+ // Also store in database if available
19
+ if (this.db) {
20
+ return await this.db.set(`oauth-${userId}`, {
21
+ userId,
22
+ ...tokens,
23
+ });
24
+ }
25
+ return true;
26
+ }
27
+ catch (err) {
28
+ console.error(`❌ [OAuthTokenStorage] Failed to store tokens for ${userId}:`, err);
29
+ return false;
30
+ }
31
+ }
32
+ /**
33
+ * Retrieves OAuth tokens for a user.
34
+ */
35
+ async getTokens(userId) {
36
+ try {
37
+ // Check in-memory store first
38
+ const cached = this.inMemoryStore.get(`oauth-${userId}`);
39
+ if (cached) {
40
+ return cached;
41
+ }
42
+ // Check database if available
43
+ if (this.db) {
44
+ const data = await this.db.get(`oauth-${userId}`);
45
+ if (data) {
46
+ const tokens = {
47
+ access_token: data.access_token,
48
+ refresh_token: data.refresh_token,
49
+ expires_at: data.expires_at,
50
+ token_type: data.token_type,
51
+ scope: data.scope,
52
+ };
53
+ // Cache in memory
54
+ this.inMemoryStore.set(`oauth-${userId}`, tokens);
55
+ return tokens;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ catch (err) {
61
+ console.error(`❌ [OAuthTokenStorage] Failed to get tokens for ${userId}:`, err);
62
+ return null;
63
+ }
64
+ }
65
+ /**
66
+ * Updates OAuth tokens for a user.
67
+ */
68
+ async updateTokens(userId, tokens) {
69
+ try {
70
+ // Update in-memory store
71
+ this.inMemoryStore.set(`oauth-${userId}`, tokens);
72
+ // Update in database if available
73
+ if (this.db) {
74
+ return await this.db.update(`oauth-${userId}`, {
75
+ ...tokens,
76
+ });
77
+ }
78
+ return true;
79
+ }
80
+ catch (err) {
81
+ console.error(`❌ [OAuthTokenStorage] Failed to update tokens for ${userId}:`, err);
82
+ return false;
83
+ }
84
+ }
85
+ /**
86
+ * Deletes OAuth tokens for a user.
87
+ */
88
+ async deleteTokens(userId) {
89
+ try {
90
+ // Remove from in-memory store
91
+ this.inMemoryStore.delete(`oauth-${userId}`);
92
+ // Remove from database if available
93
+ if (this.db) {
94
+ return await this.db.delete(`oauth-${userId}`);
95
+ }
96
+ return true;
97
+ }
98
+ catch (err) {
99
+ console.error(`❌ [OAuthTokenStorage] Failed to delete tokens for ${userId}:`, err);
100
+ return false;
101
+ }
102
+ }
103
+ /**
104
+ * Clears all in-memory tokens (useful for testing).
105
+ */
106
+ clearMemory() {
107
+ this.inMemoryStore.clear();
108
+ }
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",