@book000/pixivts-db-mysql 0.60.0 → 0.60.2

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/schema.ts","../src/connection.ts","../src/recorder.ts","../src/migrations.ts"],"mappings":";;;;;;;;;;;;;;;;;;;AAwBA;;cAAa,cAAA,mCAAc,qBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAoC1B,IAAA;;;;;;;;;;;;;;;;KAGW,WAAA,UAAqB,cAAA,CAAe,YAAY;;KAGhD,WAAA,UAAqB,cAAA,CAAe,YAAY;;;;UCvD3C,iBAAA;;ADajB;;;ECRE,IAAA;EDQyB;;;;ECFzB,IAAA;;;;;EAMA,IAAA;;;;;EAMA,QAAA;;;;;EAMA,QAAA;AAAA;;;;;;KAQU,UAAA,GAAa,UAAA,QAAkB,OAAA,QAAe,gBAAA;;;;;;;;iBAe1C,kBAAA,CAAmB,IAAA,EAAM,iBAAA;EACvC,IAAA,EAAM,KAAA,CAAM,IAAA;EACZ,EAAA,EAAI,UAAA;AAAA;;;;UCzCW,cAAA;;;;;EAKf,WAAA,EAAa,mBAAA;;EAGb,EAAA,EAAI,UAAA;;EAGJ,KAAA,IAAS,OAAA;AAAA;;UAQM,eAAA,SAAwB,iBAAiB;;;;;;;EAOxD,SAAS;AAAA;;;;;;;;;;iBA8CW,WAAA,CACpB,EAAA,EAAI,UAAA,EACJ,MAAA,EAAQ,cAAA,GACP,OAAA;;;;;;;;;iBAkCa,oBAAA,CACd,EAAA,EAAI,UAAA,EACJ,KAAA,QAAa,OAAA,SACZ,cAAA;;;;;;;;;;;;;;;;;;;iBAyBmB,sBAAA,CACpB,IAAA,EAAM,eAAA,GACL,OAAA,CAAQ,cAAA;;UAeM,cAAA;;EAEf,MAAA;;EAEA,QAAA;;EAEA,UAAA;AAAA;;UAIe,YAAA;;EAEf,IAAA;;EAEA,KAAK;AAAA;;;;;;;;;iBAWe,YAAA,CACpB,EAAA,EAAI,UAAA,EACJ,MAAA,GAAS,cAAA,EACT,KAAA,GAAQ,YAAA,GACP,OAAA,CAAQ,WAAA;;;;;;;;iBAkCW,gBAAA,CACpB,EAAA,EAAI,UAAA,EACJ,MAAA,GAAS,cAAA,GACR,OAAA;;UAuBc,iBAAA;;EAEf,MAAA;;EAEA,QAAA;;EAEA,UAAA;;EAEA,KAAA;AAAA;;;;;;;;iBAUoB,YAAA,CAAa,EAAA,EAAI,UAAA,GAAa,OAAA,CAAQ,iBAAA;;;AFrQ5D;;;;;;;;AAAA,iBGFsB,eAAA,CAAgB,EAAA,EAAI,UAAA,GAAa,OAAO"}
package/dist/index.js CHANGED
@@ -1,80 +1,113 @@
1
- import { drizzle } from 'drizzle-orm/mysql2';
2
- import mysql from 'mysql2/promise';
3
- import { mysqlTable, datetime, longtext, int, varchar, text, uniqueIndex } from 'drizzle-orm/mysql-core';
4
- import crypto from 'crypto';
5
- import { sql, and, gte, eq, desc, count } from 'drizzle-orm';
6
-
7
- var __defProp = Object.defineProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
-
13
- // src/schema.ts
14
- var schema_exports = {};
15
- __export(schema_exports, {
16
- responsesTable: () => responsesTable
17
- });
18
- var responsesTable = mysqlTable(
19
- "responses",
20
- {
21
- /** Auto-increment primary key. */
22
- id: int("id").autoincrement().primaryKey(),
23
- /** HTTP method (GET or POST). */
24
- method: varchar("method", { length: 10 }).notNull(),
25
- /** API endpoint path (e.g. /v1/illust/detail). */
26
- endpoint: varchar("endpoint", { length: 255 }).notNull(),
27
- /** Full request URL (may be null for internal requests). */
28
- url: text("url"),
29
- /** SHA-256 hash of the request URL for deduplication. */
30
- urlHash: varchar("url_hash", { length: 255 }).notNull(),
31
- /** Serialised request headers (JSON string). */
32
- requestHeaders: longtext("request_headers"),
33
- /** Request body (URL-encoded string for POST, null for GET). */
34
- requestBody: longtext("request_body"),
35
- /** Response content type ("JSON" or "TEXT"). */
36
- responseType: varchar("response_type", { length: 10 }).notNull(),
37
- /** HTTP response status code. */
38
- statusCode: int("status_code").notNull(),
39
- /** Serialised response headers (JSON string). */
40
- responseHeaders: longtext("response_headers"),
41
- /** Raw response body. */
42
- responseBody: longtext("response_body").notNull(),
43
- /** Timestamp when the record was created. */
44
- createdAt: datetime("created_at", { mode: "date", fsp: 3 }).notNull()
45
- },
46
- (table) => [
47
- uniqueIndex("idx_unique").on(
48
- table.method,
49
- table.endpoint,
50
- table.statusCode,
51
- table.urlHash
52
- )
53
- ]
54
- );
55
-
56
- // src/connection.ts
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { drizzle } from "drizzle-orm/mysql2";
3
+ import mysql from "mysql2/promise";
4
+ import { datetime, int, longtext, mysqlTable, text, uniqueIndex, varchar } from "drizzle-orm/mysql-core";
5
+ import crypto from "node:crypto";
6
+ import { and, count, desc, eq, gte, sql } from "drizzle-orm";
7
+ //#region src/schema.ts
8
+ /**
9
+ * Drizzle ORM schema for the `responses` table.
10
+ *
11
+ * Column names are kept in snake_case to match the legacy TypeORM schema
12
+ * so that existing databases can be used without migration.
13
+ */
14
+ var schema_exports = /* @__PURE__ */ __exportAll({ responsesTable: () => responsesTable });
15
+ /**
16
+ * The `responses` table stores every HTTP response returned by the pixiv API.
17
+ *
18
+ * A unique composite index on (method, endpoint, status_code, url_hash) ensures
19
+ * that each unique request/response combination is stored only once, regardless
20
+ * of when it was recorded.
21
+ */
22
+ const responsesTable = mysqlTable("responses", {
23
+ /** Auto-increment primary key. */
24
+ id: int("id").autoincrement().primaryKey(),
25
+ /** HTTP method (GET or POST). */
26
+ method: varchar("method", { length: 10 }).notNull(),
27
+ /** API endpoint path (e.g. /v1/illust/detail). */
28
+ endpoint: varchar("endpoint", { length: 255 }).notNull(),
29
+ /** Full request URL (may be null for internal requests). */
30
+ url: text("url"),
31
+ /** SHA-256 hash of the request URL for deduplication. */
32
+ urlHash: varchar("url_hash", { length: 255 }).notNull(),
33
+ /** Serialised request headers (JSON string). */
34
+ requestHeaders: longtext("request_headers"),
35
+ /** Request body (URL-encoded string for POST, null for GET). */
36
+ requestBody: longtext("request_body"),
37
+ /** Response content type ("JSON" or "TEXT"). */
38
+ responseType: varchar("response_type", { length: 10 }).notNull(),
39
+ /** HTTP response status code. */
40
+ statusCode: int("status_code").notNull(),
41
+ /** Serialised response headers (JSON string). */
42
+ responseHeaders: longtext("response_headers"),
43
+ /** Raw response body. */
44
+ responseBody: longtext("response_body").notNull(),
45
+ /** Timestamp when the record was created. */
46
+ createdAt: datetime("created_at", {
47
+ mode: "date",
48
+ fsp: 3
49
+ }).notNull()
50
+ }, (table) => [uniqueIndex("idx_unique").on(table.method, table.endpoint, table.statusCode, table.urlHash)]);
51
+ //#endregion
52
+ //#region src/connection.ts
53
+ /**
54
+ * MySQL connection factory for @book000/pixivts-db-mysql.
55
+ *
56
+ * Creates a mysql2 connection pool and wraps it in a Drizzle ORM instance.
57
+ */
57
58
  function parsePort(value) {
58
- if (!value) return 3306;
59
- const parsed = Number.parseInt(value, 10);
60
- return Number.isNaN(parsed) ? 3306 : parsed;
59
+ if (!value) return 3306;
60
+ const parsed = Number.parseInt(value, 10);
61
+ return Number.isNaN(parsed) ? 3306 : parsed;
61
62
  }
63
+ /**
64
+ * Creates a mysql2 connection pool and returns both the raw pool and the
65
+ * Drizzle ORM wrapper.
66
+ *
67
+ * @param opts - Connection options (fall back to environment variables)
68
+ * @returns `{ pool, db }` — raw pool for `close()`, db for queries
69
+ */
62
70
  function createDbConnection(opts) {
63
- const pool = mysql.createPool({
64
- host: opts.host ?? process.env.RESPONSE_DB_HOSTNAME ?? "localhost",
65
- port: opts.port ?? parsePort(process.env.RESPONSE_DB_PORT),
66
- user: opts.user ?? process.env.RESPONSE_DB_USERNAME,
67
- password: opts.password ?? process.env.RESPONSE_DB_PASSWORD,
68
- database: opts.database ?? process.env.RESPONSE_DB_DATABASE,
69
- timezone: "+09:00",
70
- supportBigNumbers: true,
71
- bigNumberStrings: true
72
- });
73
- const db = drizzle(pool, { schema: schema_exports, mode: "default" });
74
- return { pool, db };
71
+ const pool = mysql.createPool({
72
+ host: opts.host ?? process.env.RESPONSE_DB_HOSTNAME ?? "localhost",
73
+ port: opts.port ?? parsePort(process.env.RESPONSE_DB_PORT),
74
+ user: opts.user ?? process.env.RESPONSE_DB_USERNAME,
75
+ password: opts.password ?? process.env.RESPONSE_DB_PASSWORD,
76
+ database: opts.database ?? process.env.RESPONSE_DB_DATABASE,
77
+ timezone: "+09:00",
78
+ supportBigNumbers: true,
79
+ bigNumberStrings: true
80
+ });
81
+ return {
82
+ pool,
83
+ db: drizzle(pool, {
84
+ schema: schema_exports,
85
+ mode: "default"
86
+ })
87
+ };
75
88
  }
89
+ //#endregion
90
+ //#region src/migrations.ts
91
+ /**
92
+ * Schema bootstrapping for @book000/pixivts-db-mysql.
93
+ *
94
+ * Provides a `CREATE TABLE IF NOT EXISTS` helper that can be used at startup
95
+ * without requiring drizzle-kit to be installed in production.
96
+ *
97
+ * For full migrations, use:
98
+ * pnpm drizzle-kit generate
99
+ * pnpm drizzle-kit migrate
100
+ */
101
+ /**
102
+ * Creates the `responses` table if it does not already exist.
103
+ *
104
+ * This is a lightweight alternative to running drizzle-kit migrations in
105
+ * environments where the table has not been set up yet.
106
+ *
107
+ * @param db - Drizzle ORM database instance
108
+ */
76
109
  async function bootstrapSchema(db) {
77
- await db.execute(sql`
110
+ await db.execute(sql`
78
111
  CREATE TABLE IF NOT EXISTS responses (
79
112
  id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Response ID',
80
113
  method VARCHAR(10) NOT NULL COMMENT 'HTTP method',
@@ -92,77 +125,150 @@ async function bootstrapSchema(db) {
92
125
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
93
126
  `);
94
127
  }
95
-
96
- // src/recorder.ts
128
+ //#endregion
129
+ //#region src/recorder.ts
130
+ /**
131
+ * Response recorder for @book000/pixivts-db-mysql.
132
+ *
133
+ * `createResponseRecorder()` returns a `{ interceptor, db, close }` bundle:
134
+ * - `interceptor` — pass to `PixivClient.of(token, { onResponse: interceptor })`
135
+ * - `db` — the raw Drizzle instance for custom queries
136
+ * - `close()` — shuts down the connection pool
137
+ *
138
+ * The recorder uses Drizzle ORM's `onDuplicateKeyUpdate` to silently ignore
139
+ * duplicate entries (same method + endpoint + statusCode + urlHash).
140
+ */
141
+ /**
142
+ * Creates a response recorder that persists every pixiv API response to MySQL.
143
+ *
144
+ * @param opts - Connection and bootstrapping options
145
+ * @returns `{ interceptor, db, close }`
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * const { interceptor, close } = await createResponseRecorder({
150
+ * host: 'localhost',
151
+ * database: 'pixivts',
152
+ * bootstrap: true,
153
+ * })
154
+ * const client = await PixivClient.of(token, { onResponse: interceptor })
155
+ * // ...
156
+ * await close()
157
+ * ```
158
+ */
97
159
  function ninetyDaysAgo() {
98
- return new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3);
160
+ return /* @__PURE__ */ new Date(Date.now() - 2160 * 60 * 60 * 1e3);
99
161
  }
162
+ /**
163
+ * Inserts a response record into the database.
164
+ *
165
+ * If the unique composite index fires (duplicate method + endpoint + statusCode
166
+ * + urlHash), the insert is silently ignored via `ON DUPLICATE KEY UPDATE id = id`.
167
+ *
168
+ * @param db - Drizzle ORM database instance
169
+ * @param record - Response record from the HTTP client interceptor
170
+ */
100
171
  async function addResponse(db, record) {
101
- const urlToHash = record.url ?? `${record.method}:${record.endpoint}`;
102
- const urlHash = crypto.createHash("sha256").update(urlToHash).digest("hex");
103
- await db.insert(responsesTable).values({
104
- method: record.method,
105
- endpoint: record.endpoint,
106
- url: record.url,
107
- urlHash,
108
- requestHeaders: record.requestHeaders,
109
- requestBody: record.requestBody,
110
- responseType: record.responseType,
111
- statusCode: record.statusCode,
112
- responseHeaders: record.responseHeaders,
113
- responseBody: record.responseBody,
114
- createdAt: /* @__PURE__ */ new Date()
115
- }).onDuplicateKeyUpdate({ set: { id: sql`id` } });
172
+ const urlToHash = record.url ?? `${record.method}:${record.endpoint}`;
173
+ const urlHash = crypto.createHash("sha256").update(urlToHash).digest("hex");
174
+ await db.insert(responsesTable).values({
175
+ method: record.method,
176
+ endpoint: record.endpoint,
177
+ url: record.url,
178
+ urlHash,
179
+ requestHeaders: record.requestHeaders,
180
+ requestBody: record.requestBody,
181
+ responseType: record.responseType,
182
+ statusCode: record.statusCode,
183
+ responseHeaders: record.responseHeaders,
184
+ responseBody: record.responseBody,
185
+ createdAt: /* @__PURE__ */ new Date()
186
+ }).onDuplicateKeyUpdate({ set: { id: sql`id` } });
116
187
  }
188
+ /**
189
+ * Creates a `RecorderBundle` from an existing Drizzle instance.
190
+ *
191
+ * Useful for testing — pass a mock `db` and a no-op `close`.
192
+ *
193
+ * @param db - Drizzle ORM database instance
194
+ * @param close - Function that closes the underlying connection
195
+ */
117
196
  function createRecorderBundle(db, close) {
118
- const interceptor = (record) => addResponse(db, record);
119
- return { interceptor, db, close };
197
+ const interceptor = (record) => addResponse(db, record);
198
+ return {
199
+ interceptor,
200
+ db,
201
+ close
202
+ };
120
203
  }
204
+ /**
205
+ * Creates a response recorder that persists every pixiv API response to MySQL.
206
+ *
207
+ * @param opts - Connection and bootstrapping options
208
+ * @returns `{ interceptor, db, close }`
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const { interceptor, close } = await createResponseRecorder({
213
+ * host: 'localhost',
214
+ * database: 'pixivts',
215
+ * bootstrap: true,
216
+ * })
217
+ * const client = await PixivClient.of(token, { onResponse: interceptor })
218
+ * // ...
219
+ * await close()
220
+ * ```
221
+ */
121
222
  async function createResponseRecorder(opts) {
122
- const { pool, db } = createDbConnection(opts);
123
- if (opts.bootstrap) {
124
- await bootstrapSchema(db);
125
- }
126
- return createRecorderBundle(db, () => pool.end());
223
+ const { pool, db } = createDbConnection(opts);
224
+ if (opts.bootstrap) await bootstrapSchema(db);
225
+ return createRecorderBundle(db, () => pool.end());
127
226
  }
227
+ /**
228
+ * Retrieves response records from the last 90 days.
229
+ *
230
+ * @param db - Drizzle ORM database instance
231
+ * @param filter - Optional filter criteria
232
+ * @param range - Optional pagination options
233
+ * @returns Array of response rows, newest first
234
+ */
128
235
  async function getResponses(db, filter, range) {
129
- const since = ninetyDaysAgo();
130
- const limit = range?.limit ?? 100;
131
- const offset = ((range?.page ?? 1) - 1) * limit;
132
- return db.select().from(responsesTable).where(
133
- and(
134
- gte(responsesTable.createdAt, since),
135
- filter?.method ? eq(responsesTable.method, filter.method) : void 0,
136
- filter?.endpoint ? eq(responsesTable.endpoint, filter.endpoint) : void 0,
137
- filter?.statusCode ? eq(responsesTable.statusCode, filter.statusCode) : void 0
138
- )
139
- ).orderBy(desc(responsesTable.createdAt)).limit(limit).offset(offset);
236
+ const since = ninetyDaysAgo();
237
+ const limit = range?.limit ?? 100;
238
+ const offset = ((range?.page ?? 1) - 1) * limit;
239
+ return db.select().from(responsesTable).where(and(gte(responsesTable.createdAt, since), filter?.method ? eq(responsesTable.method, filter.method) : void 0, filter?.endpoint ? eq(responsesTable.endpoint, filter.endpoint) : void 0, filter?.statusCode ? eq(responsesTable.statusCode, filter.statusCode) : void 0)).orderBy(desc(responsesTable.createdAt)).limit(limit).offset(offset);
140
240
  }
241
+ /**
242
+ * Returns the total count of response records from the last 90 days.
243
+ *
244
+ * @param db - Drizzle ORM database instance
245
+ * @param filter - Optional filter criteria
246
+ * @returns Total row count matching the filter
247
+ */
141
248
  async function getResponseCount(db, filter) {
142
- const since = ninetyDaysAgo();
143
- const rows = await db.select({ value: count() }).from(responsesTable).where(
144
- and(
145
- gte(responsesTable.createdAt, since),
146
- filter?.method ? eq(responsesTable.method, filter.method) : void 0,
147
- filter?.endpoint ? eq(responsesTable.endpoint, filter.endpoint) : void 0,
148
- filter?.statusCode ? eq(responsesTable.statusCode, filter.statusCode) : void 0
149
- )
150
- );
151
- return rows[0]?.value ?? 0;
249
+ const since = ninetyDaysAgo();
250
+ return (await db.select({ value: count() }).from(responsesTable).where(and(gte(responsesTable.createdAt, since), filter?.method ? eq(responsesTable.method, filter.method) : void 0, filter?.endpoint ? eq(responsesTable.endpoint, filter.endpoint) : void 0, filter?.statusCode ? eq(responsesTable.statusCode, filter.statusCode) : void 0)))[0]?.value ?? 0;
152
251
  }
252
+ /**
253
+ * Returns all unique (method, endpoint, statusCode) combinations seen in the
254
+ * last 90 days, along with the count of matching records.
255
+ *
256
+ * @param db - Drizzle ORM database instance
257
+ * @returns Endpoints sorted by count descending
258
+ */
153
259
  async function getEndpoints(db) {
154
- const since = ninetyDaysAgo();
155
- const rows = await db.select({
156
- method: responsesTable.method,
157
- endpoint: responsesTable.endpoint,
158
- statusCode: responsesTable.statusCode,
159
- count: count()
160
- }).from(responsesTable).where(gte(responsesTable.createdAt, since)).groupBy(
161
- responsesTable.method,
162
- responsesTable.endpoint,
163
- responsesTable.statusCode
164
- ).orderBy(desc(count()));
165
- return rows.map((r) => ({ ...r, count: r.count }));
260
+ const since = ninetyDaysAgo();
261
+ return (await db.select({
262
+ method: responsesTable.method,
263
+ endpoint: responsesTable.endpoint,
264
+ statusCode: responsesTable.statusCode,
265
+ count: count()
266
+ }).from(responsesTable).where(gte(responsesTable.createdAt, since)).groupBy(responsesTable.method, responsesTable.endpoint, responsesTable.statusCode).orderBy(desc(count()))).map((r) => ({
267
+ ...r,
268
+ count: r.count
269
+ }));
166
270
  }
167
-
271
+ //#endregion
168
272
  export { addResponse, bootstrapSchema, createDbConnection, createRecorderBundle, createResponseRecorder, getEndpoints, getResponseCount, getResponses, responsesTable };
273
+
274
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/schema.ts","../src/connection.ts","../src/migrations.ts","../src/recorder.ts"],"sourcesContent":["/**\n * Drizzle ORM schema for the `responses` table.\n *\n * Column names are kept in snake_case to match the legacy TypeORM schema\n * so that existing databases can be used without migration.\n */\n\nimport {\n int,\n datetime,\n longtext,\n mysqlTable,\n text,\n varchar,\n uniqueIndex,\n} from 'drizzle-orm/mysql-core'\n\n/**\n * The `responses` table stores every HTTP response returned by the pixiv API.\n *\n * A unique composite index on (method, endpoint, status_code, url_hash) ensures\n * that each unique request/response combination is stored only once, regardless\n * of when it was recorded.\n */\nexport const responsesTable = mysqlTable(\n 'responses',\n {\n /** Auto-increment primary key. */\n id: int('id').autoincrement().primaryKey(),\n /** HTTP method (GET or POST). */\n method: varchar('method', { length: 10 }).notNull(),\n /** API endpoint path (e.g. /v1/illust/detail). */\n endpoint: varchar('endpoint', { length: 255 }).notNull(),\n /** Full request URL (may be null for internal requests). */\n url: text('url'),\n /** SHA-256 hash of the request URL for deduplication. */\n urlHash: varchar('url_hash', { length: 255 }).notNull(),\n /** Serialised request headers (JSON string). */\n requestHeaders: longtext('request_headers'),\n /** Request body (URL-encoded string for POST, null for GET). */\n requestBody: longtext('request_body'),\n /** Response content type (\"JSON\" or \"TEXT\"). */\n responseType: varchar('response_type', { length: 10 }).notNull(),\n /** HTTP response status code. */\n statusCode: int('status_code').notNull(),\n /** Serialised response headers (JSON string). */\n responseHeaders: longtext('response_headers'),\n /** Raw response body. */\n responseBody: longtext('response_body').notNull(),\n /** Timestamp when the record was created. */\n createdAt: datetime('created_at', { mode: 'date', fsp: 3 }).notNull(),\n },\n (table) => [\n uniqueIndex('idx_unique').on(\n table.method,\n table.endpoint,\n table.statusCode,\n table.urlHash\n ),\n ]\n)\n\n/** Type for inserting a new response record. */\nexport type NewResponse = typeof responsesTable.$inferInsert\n\n/** Type for a selected response record. */\nexport type ResponseRow = typeof responsesTable.$inferSelect\n","/**\n * MySQL connection factory for @book000/pixivts-db-mysql.\n *\n * Creates a mysql2 connection pool and wraps it in a Drizzle ORM instance.\n */\n\nimport { drizzle } from 'drizzle-orm/mysql2'\nimport mysql from 'mysql2/promise'\nimport * as schema from './schema'\n\n/** Options for establishing a MySQL connection. */\nexport interface ConnectionOptions {\n /**\n * Database hostname.\n * Falls back to the `RESPONSE_DB_HOSTNAME` environment variable.\n */\n host?: string\n\n /**\n * Database port.\n * Falls back to the `RESPONSE_DB_PORT` environment variable (default: 3306).\n */\n port?: number\n\n /**\n * Database username.\n * Falls back to the `RESPONSE_DB_USERNAME` environment variable.\n */\n user?: string\n\n /**\n * Database password.\n * Falls back to the `RESPONSE_DB_PASSWORD` environment variable.\n */\n password?: string\n\n /**\n * Database name.\n * Falls back to the `RESPONSE_DB_DATABASE` environment variable.\n */\n database?: string\n}\n\n/**\n * The Drizzle ORM database instance type returned by `createConnection`.\n *\n * Typed with the `schema` so that relational queries are available.\n */\nexport type DbInstance = ReturnType<typeof drizzle<typeof schema>>\n\nfunction parsePort(value: string | undefined): number {\n if (!value) return 3306\n const parsed = Number.parseInt(value, 10)\n return Number.isNaN(parsed) ? 3306 : parsed\n}\n\n/**\n * Creates a mysql2 connection pool and returns both the raw pool and the\n * Drizzle ORM wrapper.\n *\n * @param opts - Connection options (fall back to environment variables)\n * @returns `{ pool, db }` — raw pool for `close()`, db for queries\n */\nexport function createDbConnection(opts: ConnectionOptions): {\n pool: mysql.Pool\n db: DbInstance\n} {\n const pool = mysql.createPool({\n host: opts.host ?? process.env.RESPONSE_DB_HOSTNAME ?? 'localhost',\n port: opts.port ?? parsePort(process.env.RESPONSE_DB_PORT),\n user: opts.user ?? process.env.RESPONSE_DB_USERNAME,\n password: opts.password ?? process.env.RESPONSE_DB_PASSWORD,\n database: opts.database ?? process.env.RESPONSE_DB_DATABASE,\n timezone: '+09:00',\n supportBigNumbers: true,\n bigNumberStrings: true,\n })\n\n // Type assertion required: pnpm's peer-dep resolution for the patched drizzle-orm\n // creates a structurally-incompatible Pool type for the $client property.\n // The runtime value is correct; only the declaration paths differ.\n const db = drizzle(pool, { schema, mode: 'default' }) as unknown as DbInstance\n return { pool, db }\n}\n","/**\n * Schema bootstrapping for @book000/pixivts-db-mysql.\n *\n * Provides a `CREATE TABLE IF NOT EXISTS` helper that can be used at startup\n * without requiring drizzle-kit to be installed in production.\n *\n * For full migrations, use:\n * pnpm drizzle-kit generate\n * pnpm drizzle-kit migrate\n */\n\nimport { sql } from 'drizzle-orm'\nimport type { DbInstance } from './connection'\n\n/**\n * Creates the `responses` table if it does not already exist.\n *\n * This is a lightweight alternative to running drizzle-kit migrations in\n * environments where the table has not been set up yet.\n *\n * @param db - Drizzle ORM database instance\n */\nexport async function bootstrapSchema(db: DbInstance): Promise<void> {\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS responses (\n id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Response ID',\n method VARCHAR(10) NOT NULL COMMENT 'HTTP method',\n endpoint VARCHAR(255) NOT NULL COMMENT 'API endpoint path',\n url TEXT NULL COMMENT 'Full request URL',\n url_hash VARCHAR(255) NOT NULL COMMENT 'SHA-256 hash of the request URL',\n request_headers LONGTEXT NULL COMMENT 'Request headers (JSON)',\n request_body LONGTEXT NULL COMMENT 'Request body',\n response_type VARCHAR(10) NOT NULL COMMENT 'Response content type',\n status_code INT NOT NULL COMMENT 'HTTP status code',\n response_headers LONGTEXT NULL COMMENT 'Response headers (JSON)',\n response_body LONGTEXT NOT NULL COMMENT 'Response body',\n created_at DATETIME(3) NOT NULL COMMENT 'Record creation timestamp',\n UNIQUE INDEX idx_unique (method, endpoint, status_code, url_hash)\n ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci\n `)\n}\n","/**\n * Response recorder for @book000/pixivts-db-mysql.\n *\n * `createResponseRecorder()` returns a `{ interceptor, db, close }` bundle:\n * - `interceptor` — pass to `PixivClient.of(token, { onResponse: interceptor })`\n * - `db` — the raw Drizzle instance for custom queries\n * - `close()` — shuts down the connection pool\n *\n * The recorder uses Drizzle ORM's `onDuplicateKeyUpdate` to silently ignore\n * duplicate entries (same method + endpoint + statusCode + urlHash).\n */\n\nimport crypto from 'node:crypto'\nimport { and, count, desc, eq, gte, sql } from 'drizzle-orm'\nimport type { ResponseInterceptor, ResponseRecord } from '@book000/pixivts'\nimport { createDbConnection, type ConnectionOptions, type DbInstance } from './connection'\nimport { responsesTable, type ResponseRow } from './schema'\nimport { bootstrapSchema } from './migrations'\n\n// ---------------------------------------------------------------------------\n// Result bundle\n// ---------------------------------------------------------------------------\n\n/** The object returned by `createResponseRecorder()`. */\nexport interface RecorderBundle {\n /**\n * Response interceptor — pass directly to `PixivClient.of()` as\n * the `onResponse` option.\n */\n interceptor: ResponseInterceptor\n\n /** Drizzle ORM database instance for custom queries. */\n db: DbInstance\n\n /** Closes the underlying connection pool. */\n close(): Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/** Options for `createResponseRecorder()`. */\nexport interface RecorderOptions extends ConnectionOptions {\n /**\n * If `true`, runs `CREATE TABLE IF NOT EXISTS` before returning.\n * Useful for first-run bootstrapping without a separate migration step.\n *\n * @default false\n */\n bootstrap?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a response recorder that persists every pixiv API response to MySQL.\n *\n * @param opts - Connection and bootstrapping options\n * @returns `{ interceptor, db, close }`\n *\n * @example\n * ```ts\n * const { interceptor, close } = await createResponseRecorder({\n * host: 'localhost',\n * database: 'pixivts',\n * bootstrap: true,\n * })\n * const client = await PixivClient.of(token, { onResponse: interceptor })\n * // ...\n * await close()\n * ```\n */\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction ninetyDaysAgo(): Date {\n return new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)\n}\n\n// ---------------------------------------------------------------------------\n// Core DB helpers (exported for testing)\n// ---------------------------------------------------------------------------\n\n/**\n * Inserts a response record into the database.\n *\n * If the unique composite index fires (duplicate method + endpoint + statusCode\n * + urlHash), the insert is silently ignored via `ON DUPLICATE KEY UPDATE id = id`.\n *\n * @param db - Drizzle ORM database instance\n * @param record - Response record from the HTTP client interceptor\n */\nexport async function addResponse(\n db: DbInstance,\n record: ResponseRecord\n): Promise<void> {\n // Hash the URL if present; fall back to \"method:endpoint\" so the column is never empty.\n const urlToHash = record.url ?? `${record.method}:${record.endpoint}`\n const urlHash = crypto\n .createHash('sha256')\n .update(urlToHash)\n .digest('hex')\n\n await db\n .insert(responsesTable)\n .values({\n method: record.method,\n endpoint: record.endpoint,\n url: record.url,\n urlHash,\n requestHeaders: record.requestHeaders,\n requestBody: record.requestBody,\n responseType: record.responseType,\n statusCode: record.statusCode,\n responseHeaders: record.responseHeaders,\n responseBody: record.responseBody,\n createdAt: new Date(),\n })\n .onDuplicateKeyUpdate({ set: { id: sql`id` } })\n}\n\n/**\n * Creates a `RecorderBundle` from an existing Drizzle instance.\n *\n * Useful for testing — pass a mock `db` and a no-op `close`.\n *\n * @param db - Drizzle ORM database instance\n * @param close - Function that closes the underlying connection\n */\nexport function createRecorderBundle(\n db: DbInstance,\n close: () => Promise<void>\n): RecorderBundle {\n const interceptor: ResponseInterceptor = (record) =>\n addResponse(db, record)\n\n return { interceptor, db, close }\n}\n\n/**\n * Creates a response recorder that persists every pixiv API response to MySQL.\n *\n * @param opts - Connection and bootstrapping options\n * @returns `{ interceptor, db, close }`\n *\n * @example\n * ```ts\n * const { interceptor, close } = await createResponseRecorder({\n * host: 'localhost',\n * database: 'pixivts',\n * bootstrap: true,\n * })\n * const client = await PixivClient.of(token, { onResponse: interceptor })\n * // ...\n * await close()\n * ```\n */\nexport async function createResponseRecorder(\n opts: RecorderOptions\n): Promise<RecorderBundle> {\n const { pool, db } = createDbConnection(opts)\n\n if (opts.bootstrap) {\n await bootstrapSchema(db)\n }\n\n return createRecorderBundle(db, () => pool.end())\n}\n\n// ---------------------------------------------------------------------------\n// Query helpers\n// ---------------------------------------------------------------------------\n\n/** Filter criteria for response queries. */\nexport interface ResponseFilter {\n /** HTTP method to filter by (e.g. `\"GET\"`). Omit to match all methods. */\n method?: string\n /** API endpoint path to filter by (e.g. `\"/v1/illust/detail\"`). Omit to match all endpoints. */\n endpoint?: string\n /** HTTP status code to filter by (e.g. `200`). Omit to match all status codes. */\n statusCode?: number\n}\n\n/** Pagination options for `getResponses`. */\nexport interface RangeOptions {\n /** 1-based page number (default: 1). */\n page?: number\n /** Items per page (default: 100). */\n limit?: number\n}\n\n/**\n * Retrieves response records from the last 90 days.\n *\n * @param db - Drizzle ORM database instance\n * @param filter - Optional filter criteria\n * @param range - Optional pagination options\n * @returns Array of response rows, newest first\n */\nexport async function getResponses(\n db: DbInstance,\n filter?: ResponseFilter,\n range?: RangeOptions\n): Promise<ResponseRow[]> {\n const since = ninetyDaysAgo()\n const limit = range?.limit ?? 100\n const offset = ((range?.page ?? 1) - 1) * limit\n\n return db\n .select()\n .from(responsesTable)\n .where(\n and(\n gte(responsesTable.createdAt, since),\n filter?.method\n ? eq(responsesTable.method, filter.method)\n : undefined,\n filter?.endpoint\n ? eq(responsesTable.endpoint, filter.endpoint)\n : undefined,\n filter?.statusCode\n ? eq(responsesTable.statusCode, filter.statusCode)\n : undefined\n )\n )\n .orderBy(desc(responsesTable.createdAt))\n .limit(limit)\n .offset(offset)\n}\n\n/**\n * Returns the total count of response records from the last 90 days.\n *\n * @param db - Drizzle ORM database instance\n * @param filter - Optional filter criteria\n * @returns Total row count matching the filter\n */\nexport async function getResponseCount(\n db: DbInstance,\n filter?: ResponseFilter\n): Promise<number> {\n const since = ninetyDaysAgo()\n const rows = await db\n .select({ value: count() })\n .from(responsesTable)\n .where(\n and(\n gte(responsesTable.createdAt, since),\n filter?.method\n ? eq(responsesTable.method, filter.method)\n : undefined,\n filter?.endpoint\n ? eq(responsesTable.endpoint, filter.endpoint)\n : undefined,\n filter?.statusCode\n ? eq(responsesTable.statusCode, filter.statusCode)\n : undefined\n )\n )\n return rows[0]?.value ?? 0\n}\n\n/** An endpoint with its response count. */\nexport interface EndpointWithCount {\n /** HTTP method (e.g. `\"GET\"` or `\"POST\"`). */\n method: string\n /** API endpoint path (e.g. `\"/v1/illust/detail\"`). */\n endpoint: string\n /** HTTP status code returned by the endpoint. */\n statusCode: number\n /** Number of recorded responses for this (method, endpoint, statusCode) combination. */\n count: number\n}\n\n/**\n * Returns all unique (method, endpoint, statusCode) combinations seen in the\n * last 90 days, along with the count of matching records.\n *\n * @param db - Drizzle ORM database instance\n * @returns Endpoints sorted by count descending\n */\nexport async function getEndpoints(db: DbInstance): Promise<EndpointWithCount[]> {\n const since = ninetyDaysAgo()\n const rows = await db\n .select({\n method: responsesTable.method,\n endpoint: responsesTable.endpoint,\n statusCode: responsesTable.statusCode,\n count: count(),\n })\n .from(responsesTable)\n .where(gte(responsesTable.createdAt, since))\n .groupBy(\n responsesTable.method,\n responsesTable.endpoint,\n responsesTable.statusCode\n )\n .orderBy(desc(count()))\n\n return rows.map((r) => ({ ...r, count: r.count }))\n}\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@book000/pixivts-db-mysql",
3
- "version": "0.60.0",
3
+ "version": "0.60.2",
4
4
  "description": "MySQL response recorder for @book000/pixivts (Drizzle ORM)",
5
5
  "keywords": [
6
6
  "pixiv",
@@ -46,18 +46,18 @@
46
46
  "mysql2": "3.22.5"
47
47
  },
48
48
  "peerDependencies": {
49
- "@book000/pixivts": "0.60.0"
49
+ "@book000/pixivts": "0.60.2"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "24.13.2",
53
53
  "@vitest/coverage-v8": "4.1.8",
54
54
  "drizzle-kit": "0.31.10",
55
- "tsup": "8.5.1",
55
+ "tsdown": "0.22.2",
56
56
  "typescript": "6.0.3",
57
57
  "vitest": "4.1.8"
58
58
  },
59
59
  "scripts": {
60
- "build": "tsup",
60
+ "build": "tsdown",
61
61
  "clean": "rimraf dist",
62
62
  "lint": "tsc -p tsconfig.test.json",
63
63
  "fix": "echo 'no fix needed'",