supalite 0.1.5

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.
Files changed (51) hide show
  1. package/.env.example +14 -0
  2. package/.eslintrc.json +21 -0
  3. package/.prettierrc +7 -0
  4. package/CHANGELOG.md +50 -0
  5. package/CHANGE_REPORT_LOG.md +152 -0
  6. package/README.md +337 -0
  7. package/dist/__tests__/client.test.d.ts +1 -0
  8. package/dist/__tests__/client.test.js +13 -0
  9. package/dist/__tests__/postgres-client.test.d.ts +1 -0
  10. package/dist/__tests__/postgres-client.test.js +34 -0
  11. package/dist/__tests__/types.test.d.ts +1 -0
  12. package/dist/__tests__/types.test.js +12 -0
  13. package/dist/client.d.ts +8 -0
  14. package/dist/client.js +28 -0
  15. package/dist/database.types.d.ts +25 -0
  16. package/dist/database.types.js +2 -0
  17. package/dist/errors.d.ts +24 -0
  18. package/dist/errors.js +37 -0
  19. package/dist/index.d.ts +5 -0
  20. package/dist/index.js +23 -0
  21. package/dist/postgres-client.d.ts +45 -0
  22. package/dist/postgres-client.js +166 -0
  23. package/dist/query-builder.d.ts +57 -0
  24. package/dist/query-builder.js +356 -0
  25. package/dist/types.d.ts +81 -0
  26. package/dist/types.js +2 -0
  27. package/examples/README.md +82 -0
  28. package/examples/setup.sql +72 -0
  29. package/examples/test.ts +55 -0
  30. package/examples/tests/array-insert.ts +125 -0
  31. package/examples/tests/bigint.ts +516 -0
  32. package/examples/tests/in.ts +73 -0
  33. package/examples/tests/mutation.ts +124 -0
  34. package/examples/tests/select.ts +88 -0
  35. package/examples/tests/special.ts +110 -0
  36. package/examples/tests/transaction.ts +161 -0
  37. package/examples/tests/where.ts +99 -0
  38. package/examples/types/database.ts +165 -0
  39. package/jest.config.js +10 -0
  40. package/package.json +51 -0
  41. package/src/__tests__/client.test.ts +13 -0
  42. package/src/__tests__/postgres-client.test.ts +36 -0
  43. package/src/__tests__/types.test.ts +12 -0
  44. package/src/client.ts +12 -0
  45. package/src/database.types.ts +25 -0
  46. package/src/errors.ts +45 -0
  47. package/src/index.ts +8 -0
  48. package/src/postgres-client.ts +229 -0
  49. package/src/query-builder.ts +448 -0
  50. package/src/types.ts +113 -0
  51. package/tsconfig.json +21 -0
@@ -0,0 +1,448 @@
1
+ import { Pool } from 'pg';
2
+ import { PostgresError } from './errors';
3
+ import {
4
+ TableName,
5
+ QueryType,
6
+ QueryOptions,
7
+ FilterOptions,
8
+ QueryResult,
9
+ SingleQueryResult,
10
+ DatabaseSchema,
11
+ SchemaName,
12
+ Row,
13
+ InsertRow,
14
+ UpdateRow
15
+ } from './types';
16
+
17
+ export class QueryBuilder<
18
+ T extends DatabaseSchema,
19
+ S extends SchemaName<T> = 'public',
20
+ K extends TableName<T, S> = TableName<T, S>
21
+ > implements Promise<QueryResult<Row<T, S, K>> | SingleQueryResult<Row<T, S, K>>> {
22
+ readonly [Symbol.toStringTag] = 'QueryBuilder';
23
+ private table: K;
24
+ private schema: S;
25
+ private selectColumns: string | null = null;
26
+ private whereConditions: string[] = [];
27
+ private orConditions: string[][] = [];
28
+ private countOption?: 'exact' | 'planned' | 'estimated';
29
+ private headOption?: boolean;
30
+ private orderByColumns: string[] = [];
31
+ private limitValue?: number;
32
+ private offsetValue?: number;
33
+ private whereValues: any[] = [];
34
+ private isSingleResult: boolean = false;
35
+ private queryType: QueryType = 'SELECT';
36
+ private insertData?: InsertRow<T, S, K> | InsertRow<T, S, K>[];
37
+ private updateData?: UpdateRow<T, S, K>;
38
+ private conflictTarget?: string;
39
+
40
+ constructor(private pool: Pool, table: K, schema: S = 'public' as S) {
41
+ this.table = table;
42
+ this.schema = schema;
43
+ }
44
+
45
+ then<TResult1 = QueryResult<Row<T, S, K>> | SingleQueryResult<Row<T, S, K>>, TResult2 = never>(
46
+ onfulfilled?: ((value: QueryResult<Row<T, S, K>> | SingleQueryResult<Row<T, S, K>>) => TResult1 | PromiseLike<TResult1>) | null,
47
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
48
+ ): Promise<TResult1 | TResult2> {
49
+ return this.execute().then(onfulfilled, onrejected);
50
+ }
51
+
52
+ catch<TResult = never>(
53
+ onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
54
+ ): Promise<QueryResult<Row<T, S, K>> | SingleQueryResult<Row<T, S, K>> | TResult> {
55
+ return this.execute().catch(onrejected);
56
+ }
57
+
58
+ finally(onfinally?: (() => void) | null): Promise<QueryResult<Row<T, S, K>> | SingleQueryResult<Row<T, S, K>>> {
59
+ return this.execute().finally(onfinally);
60
+ }
61
+
62
+ select(
63
+ columns: string = '*',
64
+ options?: {
65
+ count?: 'exact' | 'planned' | 'estimated',
66
+ head?: boolean
67
+ }
68
+ ): this {
69
+ this.selectColumns = columns;
70
+ this.countOption = options?.count;
71
+ this.headOption = options?.head;
72
+ return this;
73
+ }
74
+
75
+ eq(column: string, value: any): this {
76
+ this.whereConditions.push(`"${column}" = $${this.whereValues.length + 1}`);
77
+ this.whereValues.push(value);
78
+ return this;
79
+ }
80
+
81
+ neq(column: string, value: any): this {
82
+ this.whereConditions.push(`"${column}" != $${this.whereValues.length + 1}`);
83
+ this.whereValues.push(value);
84
+ return this;
85
+ }
86
+
87
+ is(column: string, value: any): this {
88
+ if (value === null) {
89
+ this.whereConditions.push(`"${column}" IS NULL`);
90
+ } else {
91
+ this.whereConditions.push(`"${column}" IS $${this.whereValues.length + 1}`);
92
+ this.whereValues.push(value);
93
+ }
94
+ return this;
95
+ }
96
+
97
+ contains(column: string, value: any): this {
98
+ this.whereConditions.push(`"${column}" @> $${this.whereValues.length + 1}`);
99
+ this.whereValues.push(value);
100
+ return this;
101
+ }
102
+
103
+ in(column: string, values: any[]): this {
104
+ if (values.length === 0) {
105
+ this.whereConditions.push('FALSE');
106
+ return this;
107
+ }
108
+ const placeholders = values.map((_, i) => `$${this.whereValues.length + i + 1}`).join(',');
109
+ this.whereConditions.push(`"${column}" IN (${placeholders})`);
110
+ this.whereValues.push(...values);
111
+ return this;
112
+ }
113
+
114
+ gte(column: string, value: any): this {
115
+ this.whereConditions.push(`"${column}" >= $${this.whereValues.length + 1}`);
116
+ this.whereValues.push(value);
117
+ return this;
118
+ }
119
+
120
+ lte(column: string, value: any): this {
121
+ this.whereConditions.push(`"${column}" <= $${this.whereValues.length + 1}`);
122
+ this.whereValues.push(value);
123
+ return this;
124
+ }
125
+
126
+ order(column: string, { ascending = true }: { ascending: boolean }): this {
127
+ this.orderByColumns.push(`"${column}" ${ascending ? 'ASC' : 'DESC'}`);
128
+ return this;
129
+ }
130
+
131
+ limit(value: number): this {
132
+ this.limitValue = value;
133
+ return this;
134
+ }
135
+
136
+ offset(value: number): this {
137
+ this.offsetValue = value;
138
+ return this;
139
+ }
140
+
141
+ single(): Promise<SingleQueryResult<Row<T, S, K>>> {
142
+ this.isSingleResult = true;
143
+ return this.execute() as Promise<SingleQueryResult<Row<T, S, K>>>;
144
+ }
145
+
146
+ ilike(column: string, pattern: string): this {
147
+ this.whereConditions.push(`"${column}" ILIKE $${this.whereValues.length + 1}`);
148
+ this.whereValues.push(pattern);
149
+ return this;
150
+ }
151
+
152
+ or(conditions: string): this {
153
+ const orParts = conditions.split(',').map(condition => {
154
+ const [field, op, value] = condition.split('.');
155
+
156
+ const validOperators = ['eq', 'neq', 'ilike', 'like', 'gt', 'gte', 'lt', 'lte'];
157
+ if (!validOperators.includes(op)) {
158
+ throw new Error(`Invalid operator: ${op}`);
159
+ }
160
+
161
+ let processedValue: any = value;
162
+ if (value === 'null') {
163
+ processedValue = null;
164
+ } else if (!isNaN(Number(value))) {
165
+ processedValue = value;
166
+ } else if (value.match(/^\d{4}-\d{2}-\d{2}/)) {
167
+ processedValue = value;
168
+ }
169
+
170
+ this.whereValues.push(processedValue);
171
+ const paramIndex = this.whereValues.length;
172
+
173
+ switch (op) {
174
+ case 'eq':
175
+ return `"${field}" = $${paramIndex}`;
176
+ case 'neq':
177
+ return `"${field}" != $${paramIndex}`;
178
+ case 'ilike':
179
+ return `"${field}" ILIKE $${paramIndex}`;
180
+ case 'like':
181
+ return `"${field}" LIKE $${paramIndex}`;
182
+ case 'gt':
183
+ return `"${field}" > $${paramIndex}`;
184
+ case 'gte':
185
+ return `"${field}" >= $${paramIndex}`;
186
+ case 'lt':
187
+ return `"${field}" < $${paramIndex}`;
188
+ case 'lte':
189
+ return `"${field}" <= $${paramIndex}`;
190
+ default:
191
+ return '';
192
+ }
193
+ }).filter(Boolean);
194
+
195
+ if (orParts.length > 0) {
196
+ this.whereConditions.push(`(${orParts.join(' OR ')})`);
197
+ }
198
+ return this;
199
+ }
200
+
201
+ returns<NewS extends SchemaName<T>, NewK extends TableName<T, NewS>>(): QueryBuilder<T, NewS, NewK> {
202
+ return this as unknown as QueryBuilder<T, NewS, NewK>;
203
+ }
204
+
205
+ range(from: number, to: number): this {
206
+ this.limitValue = to - from + 1;
207
+ this.offsetValue = from;
208
+ return this;
209
+ }
210
+
211
+ upsert(
212
+ values: InsertRow<T, S, K>,
213
+ options?: { onConflict: string }
214
+ ): this {
215
+ this.queryType = 'UPSERT';
216
+ this.insertData = values;
217
+ this.conflictTarget = options?.onConflict;
218
+ return this;
219
+ }
220
+
221
+ private shouldReturnData(): boolean {
222
+ return this.selectColumns !== null;
223
+ }
224
+
225
+ private buildWhereClause(updateValues?: any[]): string {
226
+ if (this.whereConditions.length === 0) {
227
+ return '';
228
+ }
229
+
230
+ const conditions = [...this.whereConditions];
231
+ if (this.orConditions.length > 0) {
232
+ conditions.push(
233
+ this.orConditions.map(group => `(${group.join(' OR ')})`).join(' AND ')
234
+ );
235
+ }
236
+
237
+ if (updateValues) {
238
+ return ' WHERE ' + conditions
239
+ .map(cond => cond.replace(/\$(\d+)/g, (match, num) =>
240
+ `$${parseInt(num) + updateValues.length}`))
241
+ .join(' AND ');
242
+ }
243
+
244
+ return ' WHERE ' + conditions.join(' AND ');
245
+ }
246
+
247
+ private buildQuery(): { query: string; values: any[] } {
248
+ let query = '';
249
+ let values: any[] = [];
250
+ let insertColumns: string[] = [];
251
+ const returning = this.shouldReturnData() ? ` RETURNING ${this.selectColumns || '*'}` : '';
252
+ const schemaTable = `"${String(this.schema)}"."${String(this.table)}"`;
253
+
254
+ switch (this.queryType) {
255
+ case 'SELECT':
256
+ if (this.headOption) {
257
+ query = `SELECT COUNT(*) FROM ${schemaTable}`;
258
+ } else {
259
+ query = `SELECT ${this.selectColumns || '*'} FROM ${schemaTable}`;
260
+ if (this.countOption === 'exact') {
261
+ query = `SELECT *, COUNT(*) OVER() as exact_count FROM (${query}) subquery`;
262
+ }
263
+ }
264
+ values = [...this.whereValues];
265
+ break;
266
+
267
+ case 'INSERT':
268
+ case 'UPSERT':
269
+ if (!this.insertData) throw new Error('No data provided for insert/upsert');
270
+
271
+ if (Array.isArray(this.insertData)) {
272
+ const rows = this.insertData;
273
+ if (rows.length === 0) throw new Error('Empty array provided for insert');
274
+
275
+ insertColumns = Object.keys(rows[0]);
276
+ values = rows.map(row => Object.values(row)).flat();
277
+ const placeholders = rows.map((_, i) =>
278
+ `(${insertColumns.map((_, j) => `$${i * insertColumns.length + j + 1}`).join(',')})`
279
+ ).join(',');
280
+
281
+ query = `INSERT INTO ${schemaTable} ("${insertColumns.join('","')}") VALUES ${placeholders}`;
282
+ } else {
283
+ const insertData = this.insertData as Record<string, unknown>;
284
+ insertColumns = Object.keys(insertData);
285
+ values = Object.values(insertData);
286
+ const insertPlaceholders = values.map((_, i) => `$${i + 1}`).join(',');
287
+ query = `INSERT INTO ${schemaTable} ("${insertColumns.join('","')}") VALUES (${insertPlaceholders})`;
288
+ }
289
+
290
+ if (this.queryType === 'UPSERT' && this.conflictTarget) {
291
+ query += ` ON CONFLICT (${this.conflictTarget}) DO UPDATE SET `;
292
+ query += insertColumns
293
+ .map((col: string) => `"${col}" = EXCLUDED."${col}"`)
294
+ .join(', ');
295
+ }
296
+
297
+ query += returning;
298
+ break;
299
+
300
+ case 'UPDATE':
301
+ if (!this.updateData) throw new Error('No data provided for update');
302
+ const updateData = { ...this.updateData } as Record<string, unknown>;
303
+
304
+ const now = new Date().toISOString();
305
+ if ('modified_at' in updateData && !updateData.modified_at) {
306
+ updateData.modified_at = now;
307
+ }
308
+ if ('updated_at' in updateData && !updateData.updated_at) {
309
+ updateData.updated_at = now;
310
+ }
311
+
312
+ const updateValues = Object.values(updateData);
313
+ const setColumns = Object.keys(updateData).map(
314
+ (key, index) => `"${String(key)}" = $${index + 1}`
315
+ );
316
+ query = `UPDATE ${schemaTable} SET ${setColumns.join(', ')}`;
317
+ values = [...updateValues, ...this.whereValues];
318
+ query += this.buildWhereClause(updateValues);
319
+ query += returning;
320
+ break;
321
+
322
+ case 'DELETE':
323
+ query = `DELETE FROM ${schemaTable}`;
324
+ values = [...this.whereValues];
325
+ query += this.buildWhereClause();
326
+ query += returning;
327
+ break;
328
+ }
329
+
330
+ if (this.queryType === 'SELECT') {
331
+ query += this.buildWhereClause();
332
+ }
333
+
334
+ if (this.orderByColumns.length > 0 && this.queryType === 'SELECT') {
335
+ query += ` ORDER BY ${this.orderByColumns.join(', ')}`;
336
+ }
337
+
338
+ if (this.limitValue !== undefined && this.queryType === 'SELECT') {
339
+ query += ` LIMIT ${this.limitValue}`;
340
+ }
341
+
342
+ if (this.offsetValue !== undefined && this.queryType === 'SELECT') {
343
+ query += ` OFFSET ${this.offsetValue}`;
344
+ }
345
+
346
+ return { query, values };
347
+ }
348
+
349
+ async execute(): Promise<QueryResult<Row<T, S, K>> | SingleQueryResult<Row<T, S, K>>> {
350
+ try {
351
+ const { query, values } = this.buildQuery();
352
+ const result = await this.pool.query(query, values);
353
+
354
+ if (this.queryType === 'DELETE' && !this.shouldReturnData()) {
355
+ return {
356
+ data: [],
357
+ error: null,
358
+ count: result.rowCount,
359
+ status: 200,
360
+ statusText: 'OK',
361
+ } as QueryResult<Row<T, S, K>>;
362
+ }
363
+
364
+ if (this.queryType === 'UPDATE' && !this.shouldReturnData()) {
365
+ return {
366
+ data: [],
367
+ error: null,
368
+ count: result.rowCount,
369
+ status: 200,
370
+ statusText: 'OK',
371
+ } as QueryResult<Row<T, S, K>>;
372
+ }
373
+
374
+ if (this.queryType === 'INSERT' && !this.shouldReturnData()) {
375
+ return {
376
+ data: null,
377
+ error: null,
378
+ count: result.rowCount,
379
+ status: 201,
380
+ statusText: 'Created',
381
+ };
382
+ }
383
+
384
+ if (this.isSingleResult) {
385
+ if (result.rows.length > 1) {
386
+ return {
387
+ data: null,
388
+ error: new PostgresError('Multiple rows returned in single result query'),
389
+ count: result.rowCount,
390
+ status: 406,
391
+ statusText: 'Not Acceptable',
392
+ };
393
+ }
394
+
395
+ if (result.rows.length === 0) {
396
+ return {
397
+ data: null,
398
+ error: null,
399
+ count: 0,
400
+ status: 200,
401
+ statusText: 'OK',
402
+ };
403
+ }
404
+
405
+ return {
406
+ data: result.rows[0],
407
+ error: null,
408
+ count: 1,
409
+ status: 200,
410
+ statusText: 'OK',
411
+ } as SingleQueryResult<Row<T, S, K>>;
412
+ }
413
+
414
+ return {
415
+ data: result.rows.length > 0 ? result.rows : null,
416
+ error: null,
417
+ count: result.rowCount,
418
+ status: 200,
419
+ statusText: 'OK',
420
+ } as QueryResult<Row<T, S, K>>;
421
+ } catch (err: any) {
422
+ return {
423
+ data: null,
424
+ error: new PostgresError(err.message),
425
+ count: null,
426
+ status: 500,
427
+ statusText: 'Internal Server Error',
428
+ };
429
+ }
430
+ }
431
+
432
+ insert(data: InsertRow<T, S, K> | InsertRow<T, S, K>[]): this {
433
+ this.queryType = 'INSERT';
434
+ this.insertData = data;
435
+ return this;
436
+ }
437
+
438
+ update(data: UpdateRow<T, S, K>): this {
439
+ this.queryType = 'UPDATE';
440
+ this.updateData = data;
441
+ return this;
442
+ }
443
+
444
+ delete(): this {
445
+ this.queryType = 'DELETE';
446
+ return this;
447
+ }
448
+ }
package/src/types.ts ADDED
@@ -0,0 +1,113 @@
1
+ import { PostgresError } from './errors';
2
+
3
+ export type Json =
4
+ | string
5
+ | number
6
+ | bigint // BigInt 타입 추가
7
+ | boolean
8
+ | null
9
+ | { [key: string]: Json | undefined }
10
+ | Json[];
11
+
12
+ export interface TableBase {
13
+ Row: any;
14
+ Insert: any;
15
+ Update: any;
16
+ Relationships: unknown[];
17
+ }
18
+
19
+ export interface SchemaDefinition {
20
+ Tables: { [key: string]: TableBase };
21
+ Views?: { [key: string]: any };
22
+ Functions?: { [key: string]: any };
23
+ Enums?: { [key: string]: any };
24
+ CompositeTypes?: { [key: string]: any };
25
+ }
26
+
27
+ export interface DatabaseSchema {
28
+ [schema: string]: SchemaDefinition;
29
+ }
30
+
31
+ // Supabase 스타일 데이터베이스 타입을 DatabaseSchema로 변환
32
+ export type AsDatabaseSchema<T> = {
33
+ [K in keyof T]: T[K] extends { Tables: any }
34
+ ? SchemaDefinition & T[K]
35
+ : never;
36
+ };
37
+
38
+ export type SchemaName<T extends DatabaseSchema> = keyof T;
39
+ export type TableName<
40
+ T extends DatabaseSchema,
41
+ S extends SchemaName<T> = SchemaName<T>
42
+ > = keyof T[S]['Tables'];
43
+
44
+ export type Row<
45
+ T extends DatabaseSchema,
46
+ S extends SchemaName<T>,
47
+ K extends TableName<T, S>
48
+ > = T[S]['Tables'][K]['Row'];
49
+
50
+ export type InsertRow<
51
+ T extends DatabaseSchema,
52
+ S extends SchemaName<T>,
53
+ K extends TableName<T, S>
54
+ > = T[S]['Tables'][K]['Insert'];
55
+
56
+ export type UpdateRow<
57
+ T extends DatabaseSchema,
58
+ S extends SchemaName<T>,
59
+ K extends TableName<T, S>
60
+ > = T[S]['Tables'][K]['Update'] & {
61
+ modified_at?: string;
62
+ updated_at?: string;
63
+ };
64
+
65
+ export type EnumType<
66
+ T extends DatabaseSchema,
67
+ S extends SchemaName<T>,
68
+ E extends keyof NonNullable<T[S]['Enums']>
69
+ > = NonNullable<T[S]['Enums']>[E];
70
+
71
+ export interface SupaliteConfig {
72
+ connectionString?: string; // 연결 문자열(URI) 지원
73
+ user?: string;
74
+ host?: string;
75
+ database?: string;
76
+ password?: string;
77
+ port?: number;
78
+ ssl?: boolean;
79
+ schema?: string;
80
+ }
81
+
82
+ export type QueryType = 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'UPSERT';
83
+
84
+ export interface QueryOptions {
85
+ limit?: number;
86
+ offset?: number;
87
+ order?: {
88
+ column: string;
89
+ ascending?: boolean;
90
+ };
91
+ }
92
+
93
+ export interface FilterOptions {
94
+ column: string;
95
+ operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ilike';
96
+ value: any;
97
+ }
98
+
99
+ export type BaseResult = {
100
+ error: PostgresError | null;
101
+ count: number | null;
102
+ status: number;
103
+ statusText: string;
104
+ statusCode?: number;
105
+ };
106
+
107
+ export type QueryResult<T = any> = BaseResult & {
108
+ data: T[] | null;
109
+ };
110
+
111
+ export type SingleQueryResult<T = any> = BaseResult & {
112
+ data: T | null;
113
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "module": "commonjs",
5
+ "declaration": true,
6
+ "outDir": "./dist",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "moduleResolution": "node",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "baseUrl": ".",
15
+ "paths": {
16
+ "*": ["node_modules/*"]
17
+ }
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": ["node_modules", "dist", "test"]
21
+ }