tantan-typeorm-gs 1.0.0

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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/bun.lock +300 -0
  4. package/docs/authentication.md +118 -0
  5. package/docs/configuration.md +680 -0
  6. package/docs/contributing.md +177 -0
  7. package/docs/crud.md +351 -0
  8. package/docs/custom-client.md +165 -0
  9. package/docs/development.md +243 -0
  10. package/docs/entities.md +502 -0
  11. package/docs/find-options.md +296 -0
  12. package/docs/google-cloud.md +101 -0
  13. package/docs/installation.md +51 -0
  14. package/docs/integration-testing.md +178 -0
  15. package/docs/limitations.md +223 -0
  16. package/docs/pagination.md +384 -0
  17. package/docs/performance.md +171 -0
  18. package/docs/security.md +153 -0
  19. package/docs/sorting.md +0 -0
  20. package/docs/supported-features.md +310 -0
  21. package/docs/synchronization.md +203 -0
  22. package/package.json +43 -0
  23. package/src/client/index.ts +589 -0
  24. package/src/core/data-source.ts +47 -0
  25. package/src/core/driver.ts +290 -0
  26. package/src/core/error.ts +144 -0
  27. package/src/core/memory.ts +152 -0
  28. package/src/core/query/interpreter.ts +949 -0
  29. package/src/core/query/runner.ts +1297 -0
  30. package/src/core/query/types.ts +155 -0
  31. package/src/core/schema-builder.ts +64 -0
  32. package/src/core/types.ts +113 -0
  33. package/src/core/utils.ts +13 -0
  34. package/src/index.ts +3 -0
  35. package/tantan-typeorm-gs.code-workspace +8 -0
  36. package/test/base/0001-data-source.test.ts +252 -0
  37. package/test/base/0002-operator.test.ts +616 -0
  38. package/test/base/0003-select.test.ts +281 -0
  39. package/test/base/0004-aggregate.test.ts +213 -0
  40. package/test/base/0005-transcation.test.ts +1566 -0
  41. package/test/base/0006-relation.test.ts +1611 -0
  42. package/test/base/0007-logging.test.ts +182 -0
  43. package/test/base/0008-soft-delete.test.ts +649 -0
  44. package/test/google-sheets/0001-client.test.ts +1705 -0
  45. package/test/google-sheets/0002-worksheet-management.test.ts +408 -0
  46. package/test/google-sheets/0003-data-source.test.ts +1935 -0
  47. package/test/google-sheets/0004-transactions.test.ts +952 -0
  48. package/test/google-sheets/0005-operator.test.ts +1124 -0
  49. package/test/google-sheets/0006-object-criteria.test.ts +283 -0
  50. package/test/google-sheets/0007-performance.test.ts +532 -0
  51. package/test/public-api.test.ts +34 -0
  52. package/tsconfig.build.json +21 -0
  53. package/tsconfig.json +34 -0
@@ -0,0 +1,290 @@
1
+ import type {
2
+ ColumnType,
3
+ DataSource,
4
+ Driver,
5
+ EntityMetadata,
6
+ ObjectLiteral,
7
+ QueryRunner,
8
+ ReplicationMode,
9
+ Table,
10
+ TableColumn,
11
+ TableForeignKey,
12
+ View
13
+ } from "typeorm";
14
+ import type { CteCapabilities } from "typeorm/driver/types/CteCapabilities.js";
15
+ import type { DataTypeDefaults } from "typeorm/driver/types/DataTypeDefaults.js";
16
+ import type { IsolationLevel } from "typeorm/driver/types/IsolationLevel.js";
17
+ import type { MappedColumnTypes } from "typeorm/driver/types/MappedColumnTypes.js";
18
+ import type { UpsertType } from "typeorm/driver/types/UpsertType.js";
19
+ import type { SchemaBuilder } from "typeorm/schema-builder/SchemaBuilder.js";
20
+ import type { ColumnMetadata } from "typeorm/metadata/ColumnMetadata.js";
21
+ import type { ReturningType } from "typeorm/driver/types/ReturningType.js";
22
+ import type { GoogleSheetsClient } from "./types.js";
23
+ import { GoogleSheetsSchemaBuilder } from "./schema-builder.js";
24
+ import { GoogleSheetsInvalidMetadataError } from "./error.js";
25
+ import { GoogleSheetsQueryRunner } from "./query/runner.js";
26
+
27
+ export class GoogleSheetsDriver implements Driver
28
+ {
29
+ constructor(
30
+ public readonly dataSource: DataSource,
31
+ public readonly client: GoogleSheetsClient,
32
+ ) { }
33
+
34
+ version = '1.0';
35
+ database = undefined;
36
+ schema = undefined;
37
+ isReplicated = false;
38
+ treeSupport = false;
39
+ transactionSupport = 'none' as const;
40
+ supportedDataTypes: ColumnType[] = [
41
+ 'string',
42
+ 'number',
43
+ 'boolean',
44
+ 'date',
45
+ 'uuid',
46
+ 'int'
47
+ ];
48
+ supportedIsolationLevels: readonly IsolationLevel[] = [];
49
+ supportedUpsertTypes: UpsertType[] = [];
50
+ supportedOnDeleteTypes = [];
51
+ supportedOnUpdateTypes = [];
52
+ dataTypeDefaults: DataTypeDefaults = {};
53
+ spatialTypes: ColumnType[] = [];
54
+ withLengthColumnTypes: ColumnType[] = [];
55
+ withPrecisionColumnTypes: ColumnType[] = [];
56
+ withScaleColumnTypes: ColumnType[] = [];
57
+ mappedDataTypes: MappedColumnTypes = {
58
+ createDate: Date,
59
+ createDateDefault: 'CURRENT_TIMESTAMP',
60
+ updateDate: Date,
61
+ updateDateDefault: 'CURRENT_TIMESTAMP',
62
+ deleteDate: Date,
63
+ deleteDateNullable: true,
64
+ version: Number,
65
+ treeLevel: Number,
66
+ migrationId: Number,
67
+ migrationTimestamp: Number,
68
+ migrationName: String,
69
+ cacheId: Number,
70
+ cacheIdentifier: String,
71
+ cacheTime: Number,
72
+ cacheDuration: Number,
73
+ cacheQuery: String,
74
+ cacheResult: String,
75
+ metadataType: String,
76
+ metadataDatabase: String,
77
+ metadataSchema: String,
78
+ metadataTable: String,
79
+ metadataName: String,
80
+ metadataValue: String,
81
+ };
82
+ parametersPrefix = '@';
83
+ maxAliasLength = 255;
84
+ cteCapabilities: CteCapabilities = {
85
+ enabled: false,
86
+ requiresRecursiveHint: false,
87
+ };
88
+ dummyTableName = 'dual';
89
+
90
+ get options()
91
+ {
92
+ return this.dataSource.options;
93
+ }
94
+
95
+ async connect(): Promise<void>
96
+ {
97
+ await this.client.connect();
98
+ }
99
+
100
+ async afterConnect(): Promise<void> { }
101
+
102
+ async disconnect(): Promise<void>
103
+ {
104
+ await this.client.disconnect();
105
+ }
106
+
107
+ createSchemaBuilder(): SchemaBuilder
108
+ {
109
+ return new GoogleSheetsSchemaBuilder(this.dataSource, this);
110
+ }
111
+
112
+ createQueryRunner(mode: ReplicationMode): QueryRunner
113
+ {
114
+ return new GoogleSheetsQueryRunner(this, mode);
115
+ }
116
+
117
+ escapeQueryWithParameters(sql: string, parameters: ObjectLiteral): [string, any[]]
118
+ {
119
+ const values: any[] = [];
120
+
121
+ const escapedSql =
122
+ sql.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_match, parameterName: string) =>
123
+ {
124
+ const index = values.length;
125
+
126
+ values.push(parameters[parameterName],
127
+ );
128
+
129
+ return `:orm_param_${index}`;
130
+ },
131
+ );
132
+
133
+ return [escapedSql, values];
134
+ }
135
+
136
+ escape(name: string): string
137
+ {
138
+ return `"${name.replaceAll('"', '""')}"`;
139
+ }
140
+
141
+ buildTableName(tableName: string, schema?: string, database?: string): string
142
+ {
143
+ return tableName;
144
+ }
145
+
146
+ parseTableName(target: EntityMetadata | Table | View | TableForeignKey | string)
147
+ : { tableName: string; schema?: string; database?: string; }
148
+ {
149
+ if (typeof target === 'string') return { tableName: target };
150
+ if ('tableName' in target && target.tableName) return { tableName: target.tableName };
151
+ if ('name' in target && typeof target.name === 'string') return { tableName: target.name };
152
+
153
+ throw new GoogleSheetsInvalidMetadataError('Unable to parse table name.');
154
+ }
155
+
156
+ preparePersistentValue(value: any, column: ColumnMetadata)
157
+ {
158
+ return value
159
+ }
160
+
161
+ prepareHydratedValue(value: any, column: ColumnMetadata)
162
+ {
163
+ if (value === null || value === undefined)
164
+ {
165
+ return value;
166
+ }
167
+
168
+ if (column.type === Number)
169
+ {
170
+ return Number(value);
171
+ }
172
+
173
+ if (column.type === Boolean)
174
+ {
175
+ if (typeof value === 'boolean')
176
+ {
177
+ return value;
178
+ }
179
+
180
+ if (typeof value === 'string')
181
+ {
182
+ return value.toUpperCase() === 'TRUE';
183
+ }
184
+
185
+ return Boolean(value);
186
+ }
187
+
188
+ if (column.type === Date)
189
+ {
190
+ if (value instanceof Date)
191
+ {
192
+ return value;
193
+ }
194
+
195
+ return new Date(value);
196
+ }
197
+
198
+ return value;
199
+ }
200
+
201
+ normalizeType(column: {
202
+ type?: ColumnType | string;
203
+ length?: number | string;
204
+ precision?: number | null;
205
+ scale?: number;
206
+ isArray?: boolean;
207
+ }): string
208
+ {
209
+ if (column.type === String) return 'string';
210
+ if (column.type === Number) return 'number';
211
+ if (column.type === Boolean) return 'boolean';
212
+ if (column.type === Date) return 'date';
213
+
214
+ return String(column.type ?? 'string');
215
+ }
216
+
217
+ normalizeDefault(columnMetadata: ColumnMetadata): string | undefined
218
+ {
219
+ if (columnMetadata.default === undefined) return undefined;
220
+
221
+ return String(columnMetadata.default);
222
+ }
223
+
224
+ normalizeIsUnique(column: ColumnMetadata): boolean
225
+ {
226
+ return false;
227
+ }
228
+
229
+ getColumnLength(column: ColumnMetadata): string
230
+ {
231
+ return '';
232
+ }
233
+
234
+ createFullType(column: TableColumn): string
235
+ {
236
+ return column.type;
237
+ }
238
+
239
+ async obtainMasterConnection(): Promise<any>
240
+ {
241
+ return this.client
242
+ }
243
+
244
+ async obtainSlaveConnection(): Promise<any>
245
+ {
246
+ return this.client
247
+ }
248
+
249
+ createGeneratedMap(metadata: EntityMetadata, insertResult: any, entityIndex?: number, entityNum?: number)
250
+ : ObjectLiteral | undefined
251
+ {
252
+ if (!insertResult) return undefined;
253
+
254
+ const generatedMap: ObjectLiteral = {};
255
+
256
+ for (const column of metadata.generatedColumns)
257
+ {
258
+ const value = insertResult[column.databaseName];
259
+
260
+ if (value !== undefined) generatedMap[column.propertyName] = value;
261
+ }
262
+
263
+ return generatedMap;
264
+ }
265
+
266
+ findChangedColumns(tableColumns: TableColumn[], columnMetadatas: ColumnMetadata[]): ColumnMetadata[]
267
+ {
268
+ return []
269
+ }
270
+
271
+ isReturningSqlSupported(returningType: ReturningType): boolean
272
+ {
273
+ return false
274
+ }
275
+
276
+ isUUIDGenerationSupported(): boolean
277
+ {
278
+ return true
279
+ }
280
+
281
+ isFullTextColumnTypeSupported(): boolean
282
+ {
283
+ return false
284
+ }
285
+
286
+ createParameter(parameterName: string, index: number): string
287
+ {
288
+ return `:${parameterName}`;
289
+ }
290
+ }
@@ -0,0 +1,144 @@
1
+ export class GoogleSheetsDriverError extends Error
2
+ {
3
+ constructor(
4
+ message: string,
5
+ options?: ErrorOptions,
6
+ )
7
+ {
8
+ super(message, options);
9
+
10
+ this.name = 'GoogleSheetsDriverError';
11
+ }
12
+ }
13
+
14
+ export class GoogleSheetsAuthenticationError extends GoogleSheetsDriverError
15
+ {
16
+ constructor(
17
+ message = 'Google Sheets authentication failed.',
18
+ options?: ErrorOptions,
19
+ )
20
+ {
21
+ super(message, options);
22
+
23
+ this.name = 'GoogleSheetsAuthenticationError';
24
+ }
25
+ }
26
+
27
+ export class GoogleSheetsSpreadsheetNotFoundError extends GoogleSheetsDriverError
28
+ {
29
+ constructor(
30
+ message = 'Google Sheets spreadsheet not found.',
31
+ options?: ErrorOptions,
32
+ )
33
+ {
34
+ super(message, options);
35
+
36
+ this.name = 'GoogleSheetsSpreadsheetNotFoundError';
37
+ }
38
+ }
39
+
40
+ export class GoogleSheetsWorksheetNotFoundError extends GoogleSheetsDriverError
41
+ {
42
+ constructor(
43
+ message: string,
44
+ options?: ErrorOptions,
45
+ )
46
+ {
47
+ super(message, options);
48
+
49
+ this.name = 'GoogleSheetsWorksheetNotFoundError';
50
+ }
51
+ }
52
+
53
+ export class GoogleSheetsParseError extends GoogleSheetsDriverError
54
+ {
55
+ constructor(
56
+ message: string,
57
+ options?: ErrorOptions,
58
+ )
59
+ {
60
+ super(message, options);
61
+
62
+ this.name = 'GoogleSheetsParseError';
63
+ }
64
+ }
65
+
66
+ export class GoogleSheetsInvalidRangeError extends GoogleSheetsDriverError
67
+ {
68
+ constructor(
69
+ message = 'Google Sheets range is invalid.',
70
+ options?: ErrorOptions,
71
+ )
72
+ {
73
+ super(message, options);
74
+
75
+ this.name = 'GoogleSheetsInvalidRangeError';
76
+ }
77
+ }
78
+
79
+ export class GoogleSheetsInvalidMetadataError extends GoogleSheetsDriverError
80
+ {
81
+ constructor(
82
+ message = 'Google Sheets metadata is invalid.',
83
+ options?: ErrorOptions,
84
+ )
85
+ {
86
+ super(message, options);
87
+
88
+ this.name = 'GoogleSheetsInvalidMetadataError';
89
+ }
90
+ }
91
+
92
+ export class GoogleSheetsDuplicatePrimaryKeyError extends GoogleSheetsDriverError
93
+ {
94
+ constructor(
95
+ message = 'Duplicate primary key.',
96
+ options?: ErrorOptions,
97
+ )
98
+ {
99
+ super(message, options);
100
+
101
+ this.name = 'GoogleSheetsDuplicatePrimaryKeyError';
102
+ }
103
+ }
104
+
105
+ export class GoogleSheetsMissingPrimaryKeyError extends GoogleSheetsDriverError
106
+ {
107
+ constructor(
108
+ message = 'Missing primary key.',
109
+ options?: ErrorOptions,
110
+ )
111
+ {
112
+ super(message, options);
113
+ this.name = 'GoogleSheetsMissingPrimaryKeyError';
114
+ }
115
+ }
116
+
117
+ export class GoogleSheetsUnsupportedOperationError extends GoogleSheetsDriverError
118
+ {
119
+ constructor(
120
+ operation: string,
121
+ options?: ErrorOptions,
122
+ )
123
+ {
124
+ super(
125
+ `Google Sheets operation "${operation}" is not supported.`,
126
+ options,
127
+ );
128
+
129
+ this.name = 'GoogleSheetsUnsupportedOperationError';
130
+ }
131
+ }
132
+
133
+ export class GoogleSheetsGenaralError extends GoogleSheetsDriverError
134
+ {
135
+ constructor(
136
+ message: string,
137
+ options?: ErrorOptions,
138
+ )
139
+ {
140
+ super(message, options);
141
+
142
+ this.name = 'GoogleSheetsGenaralError';
143
+ }
144
+ }
@@ -0,0 +1,152 @@
1
+ import type { GoogleSheetsClient, GoogleSheetsRow, GoogleSheetsSheetMetadata } from "./types.js";
2
+ import { validateWorksheetName } from "./utils.js";
3
+
4
+ export class Memory implements GoogleSheetsClient
5
+ {
6
+ private readonly sheets = new Map<string, GoogleSheetsRow[]>();
7
+
8
+ private readonly headers = new Map<string, string[]>();
9
+
10
+ private readonly sheetMetadata = new Map<string, GoogleSheetsSheetMetadata>();
11
+
12
+ constructor(
13
+ initialSheets: Record<string, GoogleSheetsRow[]> = {},
14
+ )
15
+ {
16
+ let sheetId = 1;
17
+
18
+ for (const [sheetName, rows] of Object.entries(initialSheets))
19
+ {
20
+ this.sheets.set(sheetName, structuredClone(rows));
21
+
22
+ this.sheetMetadata.set(sheetName, { sheetId, title: sheetName });
23
+
24
+ sheetId++;
25
+ }
26
+ }
27
+
28
+ async connect(): Promise<void> { }
29
+
30
+ async disconnect(): Promise<void> { }
31
+
32
+ async hasSheet(sheetName: string): Promise<boolean>
33
+ {
34
+ return this.sheets.has(sheetName);
35
+ }
36
+
37
+ async createSheet(sheetName: string): Promise<void>
38
+ {
39
+ validateWorksheetName(sheetName);
40
+
41
+ if (this.sheets.has(sheetName)) return;
42
+
43
+ this.sheets.set(sheetName, []);
44
+
45
+ this.headers.set(sheetName, []);
46
+
47
+ this.sheetMetadata.set(sheetName, { sheetId: this.sheetMetadata.size + 1, title: sheetName });
48
+ }
49
+
50
+ async getSheetMetadata(sheetName: string): Promise<GoogleSheetsSheetMetadata | null>
51
+ {
52
+ const metadata = this.sheetMetadata.get(sheetName);
53
+
54
+ if (!metadata) return null;
55
+
56
+ return structuredClone(metadata);
57
+ }
58
+
59
+ async getHeaders(sheetName: string): Promise<string[]>
60
+ {
61
+ return structuredClone(this.headers.get(sheetName) ?? []);
62
+ }
63
+
64
+ async insertHeaders(sheetName: string, headers: string[]): Promise<void>
65
+ {
66
+ this.headers.set(sheetName, structuredClone(headers));
67
+ }
68
+
69
+ async renameSheet(oldSheetName: string, newSheetName: string): Promise<void>
70
+ {
71
+ validateWorksheetName(newSheetName);
72
+
73
+ if (!this.sheets.has(oldSheetName)) return;
74
+
75
+ const rows = this.sheets.get(oldSheetName)!;
76
+ const headers = this.headers.get(oldSheetName) ?? [];
77
+ const metadata = this.sheetMetadata.get(oldSheetName);
78
+
79
+ this.sheets.delete(oldSheetName);
80
+ this.headers.delete(oldSheetName);
81
+ this.sheetMetadata.delete(oldSheetName);
82
+
83
+ this.sheets.set(newSheetName, rows);
84
+ this.headers.set(newSheetName, headers);
85
+
86
+ if (metadata) this.sheetMetadata.set(newSheetName, { ...metadata, title: newSheetName });
87
+ }
88
+
89
+ async deleteSheet(sheetName: string): Promise<void>
90
+ {
91
+ this.sheets.delete(sheetName);
92
+ this.headers.delete(sheetName);
93
+ this.sheetMetadata.delete(sheetName);
94
+ }
95
+
96
+ async getRows(sheetName: string): Promise<GoogleSheetsRow[]>
97
+ {
98
+ return structuredClone(this.sheets.get(sheetName) ?? []);
99
+ }
100
+
101
+ async insertRows(sheetName: string, rows: GoogleSheetsRow[]): Promise<void>
102
+ {
103
+ const currentRows = this.sheets.get(sheetName) ?? [];
104
+
105
+ currentRows.push(...structuredClone(rows));
106
+
107
+ this.sheets.set(sheetName, currentRows);
108
+ }
109
+
110
+ async updateRows(sheetName: string, predicate: (row: GoogleSheetsRow) => boolean, update: Partial<GoogleSheetsRow>)
111
+ : Promise<number>
112
+ {
113
+ const rows = this.sheets.get(sheetName) ?? [];
114
+
115
+ let affected = 0;
116
+
117
+ for (const row of rows)
118
+ {
119
+ if (!predicate(row)) continue;
120
+
121
+ Object.assign(row, update);
122
+
123
+ affected++;
124
+ }
125
+
126
+ return affected;
127
+ }
128
+
129
+ async deleteRows(sheetName: string, predicate: (row: GoogleSheetsRow) => boolean): Promise<number>
130
+ {
131
+ const rows = this.sheets.get(sheetName) ?? [];
132
+
133
+ const remainingRows: GoogleSheetsRow[] = [];
134
+ let affected = 0;
135
+
136
+ for (const row of rows)
137
+ {
138
+ if (predicate(row))
139
+ {
140
+ affected++;
141
+
142
+ continue;
143
+ }
144
+
145
+ remainingRows.push(row);
146
+ }
147
+
148
+ this.sheets.set(sheetName, remainingRows);
149
+
150
+ return affected;
151
+ }
152
+ }