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,155 @@
1
+ import type { GoogleSheetsRow } from "../types.js";
2
+
3
+ export interface InsertQuery
4
+ {
5
+ columns: string[];
6
+ values: string[][];
7
+ }
8
+
9
+ export interface UpdateQuery
10
+ {
11
+ table: string;
12
+ assignments: {
13
+ column: string;
14
+ value: string;
15
+ }[];
16
+ where: WhereExpression;
17
+ }
18
+
19
+ export interface DeleteQuery
20
+ {
21
+ table: string;
22
+ where: WhereExpression;
23
+ }
24
+
25
+ export interface SelectColumn
26
+ {
27
+ tableAlias: string;
28
+ column: string;
29
+ alias: string;
30
+ }
31
+
32
+ export interface FromQuery
33
+ {
34
+ table: string;
35
+ alias: string;
36
+ }
37
+
38
+ export type JoinType =
39
+ | 'LEFT'
40
+ | 'INNER';
41
+
42
+ export type JoinOperand =
43
+ | {
44
+ type: 'column';
45
+ tableAlias: string;
46
+ column: string;
47
+ }
48
+ | {
49
+ type: 'parameter';
50
+ value: string;
51
+ };
52
+
53
+ export type JoinConditionOperator =
54
+ | '='
55
+ | 'IS NULL'
56
+ | 'IS NOT NULL';
57
+
58
+ export interface JoinCondition
59
+ {
60
+ left: JoinOperand;
61
+ operator: JoinConditionOperator;
62
+ right?: JoinOperand;
63
+ }
64
+
65
+ export interface JoinQuery
66
+ {
67
+ type: JoinType;
68
+ table: string;
69
+ alias: string;
70
+ conditions: JoinCondition[];
71
+ }
72
+
73
+ export type JoinedRow = Record<string, GoogleSheetsRow | undefined>;
74
+
75
+ /**
76
+ * Operators generated by TypeORM FindOperator
77
+ * that can be evaluated by the Google Sheets driver.
78
+ */
79
+ export type WhereOperator =
80
+ | '='
81
+ | '!='
82
+ | '<>'
83
+ | '>'
84
+ | '>='
85
+ | '<'
86
+ | '<='
87
+ | 'IS NULL'
88
+ | 'IS NOT NULL'
89
+ | 'IN'
90
+ | 'NOT IN'
91
+ | 'ANY'
92
+ | 'LIKE'
93
+ | 'NOT LIKE'
94
+ | 'ILIKE'
95
+ | 'NOT ILIKE'
96
+ | 'BETWEEN';
97
+
98
+ export interface WhereCondition
99
+ {
100
+ tableAlias: string;
101
+ column: string;
102
+ operator: WhereOperator;
103
+ parameters: string[];
104
+ }
105
+
106
+
107
+ export type WhereExpression =
108
+ | {
109
+ type: 'condition';
110
+ condition: WhereCondition;
111
+ }
112
+ | {
113
+ type: 'and';
114
+ expressions: WhereExpression[];
115
+ }
116
+ | {
117
+ type: 'or';
118
+ expressions: WhereExpression[];
119
+ }
120
+ | {
121
+ type: 'not';
122
+ expression: WhereExpression;
123
+ };
124
+
125
+
126
+ export type AggregateFunction =
127
+ | 'COUNT'
128
+ | 'SUM'
129
+ | 'AVG'
130
+ | 'MIN'
131
+ | 'MAX';
132
+
133
+ export interface AggregateColumn
134
+ {
135
+ function: AggregateFunction;
136
+ argument: string;
137
+ alias: string;
138
+ }
139
+
140
+ export type OrderDirection =
141
+ | 'ASC'
142
+ | 'DESC';
143
+
144
+ export interface OrderBy
145
+ {
146
+ tableAlias: string;
147
+ column: string;
148
+ direction: OrderDirection;
149
+ }
150
+
151
+ export interface Pagination
152
+ {
153
+ limit?: number;
154
+ offset?: number;
155
+ }
@@ -0,0 +1,64 @@
1
+ import type { DataSource } from "typeorm";
2
+ import type { SqlInMemory } from "typeorm/driver/SqlInMemory.js";
3
+ import type { SchemaBuilder } from "typeorm/schema-builder/SchemaBuilder.js";
4
+ import type { GoogleSheetsDriver } from "./driver.js";
5
+
6
+ export class GoogleSheetsSchemaBuilder implements SchemaBuilder
7
+ {
8
+ constructor(
9
+ private readonly connection: DataSource,
10
+ private readonly driver: GoogleSheetsDriver
11
+ ) { }
12
+
13
+ async build(): Promise<void>
14
+ {
15
+ const metadatas = this.connection.entityMetadatas;
16
+
17
+ for (const metadata of metadatas)
18
+ {
19
+ if (!metadata.synchronize) continue;
20
+
21
+ const sheetName =
22
+ this.driver.buildTableName(
23
+ metadata.tableName,
24
+ metadata.schema,
25
+ metadata.database,
26
+ );
27
+
28
+ const exists = await this.driver.client.hasSheet(sheetName);
29
+
30
+ if (!exists)
31
+ {
32
+ await this.driver.client.createSheet(sheetName);
33
+
34
+ this.connection.logger.logSchemaBuild(`Created sheet "${sheetName}"`);
35
+
36
+ const headers = metadata.columns.map((column) => column.databaseName);
37
+
38
+ await this.driver.client.insertHeaders(sheetName, headers);
39
+
40
+ this.connection.logger.logSchemaBuild(`Created columns for "${sheetName}": ${headers.join(', ')}`);
41
+
42
+ continue;
43
+ }
44
+
45
+ const existingHeaders = await this.driver.client.getHeaders(sheetName);
46
+
47
+ const metadataHeaders = metadata.columns.map((column) => column.databaseName);
48
+
49
+ const missingHeaders = metadataHeaders.filter((header) => !existingHeaders.includes(header));
50
+
51
+ if (missingHeaders.length === 0) continue;
52
+
53
+ await this.driver.client.insertHeaders(sheetName, [...existingHeaders, ...missingHeaders]);
54
+
55
+ this.connection.logger.logSchemaBuild(`Added columns to "${sheetName}": ${missingHeaders.join(', ')}`);
56
+ }
57
+ }
58
+
59
+ log(): Promise<SqlInMemory>
60
+ {
61
+ throw new Error("Method not implemented.");
62
+ }
63
+
64
+ }
@@ -0,0 +1,113 @@
1
+ import type { DataSourceOptions } from "typeorm";
2
+
3
+ export interface GoogleSheetsDataSourceOptionsBase
4
+ {
5
+ type: 'google-sheets';
6
+
7
+ entities?: DataSourceOptions['entities'];
8
+ subscribers?: DataSourceOptions['subscribers'];
9
+ migrations?: DataSourceOptions['migrations'];
10
+
11
+ synchronize?: boolean;
12
+ migrationsRun?: boolean;
13
+ dropSchema?: boolean;
14
+
15
+ logging?: DataSourceOptions['logging'];
16
+ logger?: DataSourceOptions['logger'];
17
+
18
+ name?: string;
19
+ }
20
+
21
+ export interface GoogleSheetsCredentials
22
+ {
23
+ clientEmail: string;
24
+ privateKey: string;
25
+ }
26
+
27
+ export interface GoogleSheetsConsumeOptions
28
+ {
29
+ spreadsheetId: string;
30
+ credentials: GoogleSheetsCredentials;
31
+ }
32
+
33
+ export interface GoogleSheetsDataSourceWithClientOptions extends GoogleSheetsDataSourceOptionsBase
34
+ {
35
+ client: GoogleSheetsClient;
36
+ }
37
+
38
+ export interface GoogleSheetsDataSourceWithCredentialsOptions extends GoogleSheetsDataSourceOptionsBase
39
+ {
40
+ spreadsheetId: string;
41
+ credentials: GoogleSheetsCredentials;
42
+
43
+ client?: GoogleSheetsClient;
44
+ }
45
+
46
+ export type GoogleSheetsDataSourceOptions =
47
+ | GoogleSheetsDataSourceWithClientOptions
48
+ | GoogleSheetsDataSourceWithCredentialsOptions;
49
+
50
+ export interface GoogleSheetsClient
51
+ {
52
+ connect(): Promise<void>;
53
+
54
+ disconnect(): Promise<void>;
55
+
56
+ hasSheet(
57
+ sheetName: string,
58
+ ): Promise<boolean>;
59
+
60
+ createSheet(sheetName: string): Promise<void>;
61
+
62
+ getSheetMetadata(
63
+ sheetName: string,
64
+ ): Promise<GoogleSheetsSheetMetadata | null>;
65
+
66
+ getHeaders(
67
+ sheetName: string,
68
+ ): Promise<string[]>;
69
+
70
+ insertHeaders(
71
+ sheetName: string,
72
+ headers: string[],
73
+ ): Promise<void>;
74
+
75
+ renameSheet(
76
+ oldSheetName: string,
77
+ newSheetName: string,
78
+ ): Promise<void>;
79
+
80
+ deleteSheet(
81
+ sheetName: string,
82
+ ): Promise<void>;
83
+
84
+ getRows(
85
+ sheetName: string,
86
+ ): Promise<GoogleSheetsRow[]>;
87
+
88
+ insertRows(
89
+ sheetName: string,
90
+ rows: GoogleSheetsRow[],
91
+ ): Promise<void>;
92
+
93
+ updateRows(
94
+ sheetName: string,
95
+ predicate: (row: GoogleSheetsRow) => boolean,
96
+ update: Partial<GoogleSheetsRow>,
97
+ ): Promise<number>;
98
+
99
+ deleteRows(
100
+ sheetName: string,
101
+ predicate: (row: GoogleSheetsRow) => boolean,
102
+ ): Promise<number>;
103
+ }
104
+
105
+ export type GoogleSheetsRow = Record<string, unknown>;
106
+
107
+ export interface GoogleSheetsSheetMetadata
108
+ {
109
+ sheetId: number;
110
+ title: string;
111
+ }
112
+
113
+ export const DEFAULT_MAX_RETRIES = 3;
@@ -0,0 +1,13 @@
1
+ export function validateWorksheetName(sheetName: string): void
2
+ {
3
+ if (sheetName.length === 0) throw new Error('Worksheet name cannot be empty.');
4
+ if (sheetName.length > 100) throw new Error('Worksheet name cannot exceed 100 characters.');
5
+ if (/[:\\/?*\[\]]/.test(sheetName)) throw new Error('Worksheet name contains invalid characters.');
6
+ }
7
+
8
+ export function buildWorksheetRange(sheetName: string, range: string): string
9
+ {
10
+ const escapedSheetName = `'${sheetName.replace(/'/g, "''")}'`;
11
+
12
+ return `${escapedSheetName}!${range}`;
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type { GoogleSheetsDataSourceOptions, GoogleSheetsClient, GoogleSheetsRow } from './core/types.js';
2
+ export { createGoogleSheetsDataSource } from './core/data-source.js';
3
+ export { GoogleSheetsConsume } from './client/index.js';
@@ -0,0 +1,8 @@
1
+ {
2
+ "folders": [
3
+ {
4
+ "path": "."
5
+ }
6
+ ],
7
+ "settings": {}
8
+ }
@@ -0,0 +1,252 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
3
+ import { Memory } from '../../src/core/memory';
4
+ import { createGoogleSheetsDataSource } from '../../src/core/data-source';
5
+
6
+ describe('GoogleSheetsDataSource', () =>
7
+ {
8
+ @Entity()
9
+ class User
10
+ {
11
+ @PrimaryGeneratedColumn()
12
+ id!: number;
13
+
14
+ @Column()
15
+ name!: string;
16
+ }
17
+
18
+ @Entity()
19
+ class DataTypeUser
20
+ {
21
+ @PrimaryGeneratedColumn()
22
+ id!: number;
23
+
24
+ @Column()
25
+ name!: string;
26
+
27
+ @Column()
28
+ age!: number;
29
+
30
+ @Column()
31
+ active!: boolean;
32
+
33
+ @Column()
34
+ createdAt!: Date;
35
+ }
36
+
37
+ @Entity()
38
+ class TimestampUser
39
+ {
40
+ @PrimaryGeneratedColumn()
41
+ id!: number;
42
+
43
+ @Column()
44
+ name!: string;
45
+
46
+ @CreateDateColumn()
47
+ createdAt!: Date;
48
+
49
+ @UpdateDateColumn()
50
+ updatedAt!: Date;
51
+ }
52
+
53
+ test('creates DataSource with GoogleSheetsDriver', () =>
54
+ {
55
+ const dataSource = createGoogleSheetsDataSource({
56
+ type: 'google-sheets',
57
+ client: new Memory(),
58
+ });
59
+
60
+ expect(dataSource.driver.constructor.name)
61
+ .toBe('GoogleSheetsDriver');
62
+
63
+ expect(dataSource.manager.constructor.name)
64
+ .toBe('EntityManager');
65
+ });
66
+
67
+ test('initializes and builds entity metadata', async () =>
68
+ {
69
+ const dataSource = createGoogleSheetsDataSource({
70
+ type: 'google-sheets',
71
+ client: new Memory(),
72
+ entities: [User],
73
+ });
74
+
75
+ await dataSource.initialize();
76
+
77
+ expect(dataSource.isInitialized).toBe(true);
78
+ expect(dataSource.entityMetadatas).toHaveLength(1);
79
+
80
+ const metadata = dataSource.getMetadata(User);
81
+
82
+ expect(metadata.tableName).toBe('user');
83
+ expect(metadata.columns).toHaveLength(2);
84
+
85
+ await dataSource.destroy();
86
+ });
87
+
88
+ test('creates repository', async () =>
89
+ {
90
+ const dataSource = createGoogleSheetsDataSource({
91
+ type: 'google-sheets',
92
+ client: new Memory(),
93
+ entities: [User],
94
+ });
95
+
96
+ await dataSource.initialize();
97
+
98
+ const repository = dataSource.getRepository(User);
99
+
100
+ expect(repository.metadata).toBe(dataSource.getMetadata(User));
101
+ expect(repository.target).toBe(User);
102
+
103
+ await dataSource.destroy();
104
+ });
105
+
106
+ test('saves supported column data types', async () =>
107
+ {
108
+ const client =
109
+ new Memory();
110
+
111
+ const dataSource =
112
+ createGoogleSheetsDataSource({
113
+ type: 'google-sheets',
114
+ client,
115
+ entities: [
116
+ DataTypeUser,
117
+ ]
118
+ });
119
+
120
+ await dataSource.initialize();
121
+
122
+ const user =
123
+ dataSource
124
+ .getRepository(DataTypeUser)
125
+ .create({
126
+ name: 'John',
127
+ age: 30,
128
+ active: true,
129
+ createdAt: new Date(),
130
+ });
131
+
132
+ await dataSource
133
+ .getRepository(DataTypeUser)
134
+ .save(user);
135
+
136
+ const result =
137
+ await dataSource
138
+ .getRepository(DataTypeUser)
139
+ .findOneBy({
140
+ id: user.id,
141
+ });
142
+
143
+ expect(result)
144
+ .toBeDefined();
145
+
146
+ expect(result?.name)
147
+ .toBe('John');
148
+
149
+ expect(result?.age)
150
+ .toBe(30);
151
+
152
+ expect(result?.active)
153
+ .toBe(true);
154
+
155
+ expect(result?.createdAt)
156
+ .toEqual(user.createdAt);
157
+ });
158
+
159
+ test('assigns create and update dates when saving a new entity', async () =>
160
+ {
161
+ const client =
162
+ new Memory();
163
+
164
+ const dataSource =
165
+ createGoogleSheetsDataSource({
166
+ type: 'google-sheets',
167
+ client,
168
+ entities: [
169
+ TimestampUser,
170
+ ]
171
+ });
172
+
173
+ await dataSource.initialize();
174
+
175
+ const user =
176
+ dataSource
177
+ .getRepository(TimestampUser)
178
+ .create({
179
+ name: 'John',
180
+ });
181
+
182
+ await dataSource
183
+ .getRepository(TimestampUser)
184
+ .save(user);
185
+
186
+ expect(user.createdAt)
187
+ .toBeInstanceOf(Date);
188
+
189
+ expect(user.updatedAt)
190
+ .toBeInstanceOf(Date);
191
+
192
+ expect(user.createdAt.getTime())
193
+ .toBeLessThanOrEqual(
194
+ user.updatedAt.getTime(),
195
+ );
196
+ });
197
+
198
+ test('updates update date when updating an existing entity', async () =>
199
+ {
200
+ const client =
201
+ new Memory();
202
+
203
+ const dataSource =
204
+ createGoogleSheetsDataSource({
205
+ type: 'google-sheets',
206
+ client,
207
+ entities: [
208
+ TimestampUser,
209
+ ]
210
+ });
211
+
212
+ await dataSource.initialize();
213
+
214
+ const repository =
215
+ dataSource.getRepository(TimestampUser);
216
+
217
+ const user =
218
+ repository.create({
219
+ name: 'John',
220
+ });
221
+
222
+ await repository.save(user);
223
+
224
+ const createdAt =
225
+ user.createdAt.getTime();
226
+
227
+ const updatedAt =
228
+ user.updatedAt.getTime();
229
+
230
+ await new Promise(
231
+ (resolve) =>
232
+ setTimeout(resolve, 5),
233
+ );
234
+
235
+ user.name = 'Jane';
236
+
237
+ await repository.save(user);
238
+
239
+ expect(user.createdAt)
240
+ .toBeInstanceOf(Date);
241
+
242
+ expect(user.updatedAt)
243
+ .toBeInstanceOf(Date);
244
+
245
+ expect(user.createdAt.getTime())
246
+ .toBe(createdAt);
247
+
248
+ expect(user.updatedAt.getTime())
249
+ .toBeGreaterThan(updatedAt);
250
+ });
251
+
252
+ })