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,177 @@
1
+ ## Contribution Guide
2
+
3
+ Contributions to `tantan-typeorm-gs` should preserve compatibility with TypeORM while keeping the Google Sheets-specific implementation predictable and testable.
4
+
5
+ ### Before Contributing
6
+
7
+ Before making changes:
8
+
9
+ 1. Understand the existing architecture.
10
+ 2. Check whether the requested behavior is already supported.
11
+ 3. Identify the layer responsible for the change.
12
+ 4. Review existing tests related to that behavior.
13
+ 5. Avoid modifying unrelated code.
14
+
15
+ The project favors small, focused changes over broad refactoring.
16
+
17
+ ### Development Workflow
18
+
19
+ A typical contribution workflow is:
20
+
21
+ ```text
22
+ Understand
23
+ ↓
24
+ Identify affected layer
25
+ ↓
26
+ Add or update focused test
27
+ ↓
28
+ Implement change
29
+ ↓
30
+ Run focused test
31
+ ↓
32
+ Run typecheck
33
+ ↓
34
+ Run full test suite
35
+ ↓
36
+ Update documentation
37
+ ```
38
+
39
+ ### Tests First
40
+
41
+ New behavior should have an appropriate test.
42
+
43
+ Use the existing test layer that matches the behavior being changed.
44
+
45
+ | Change | Preferred test |
46
+ | ------------------------------ | ---------------------- |
47
+ | Repository behavior | Repository test |
48
+ | QueryRunner behavior | QueryRunner test |
49
+ | Query interpretation | Query/interpreter test |
50
+ | Google Sheets API behavior | API integration test |
51
+ | Performance-sensitive behavior | Performance test |
52
+
53
+ Do not add duplicate tests when an existing test already covers the same behavior.
54
+
55
+ ### Preserve Existing Behavior
56
+
57
+ The existing test suite represents supported behavior and compatibility expectations.
58
+
59
+ A contribution should not modify an existing base test merely to make a new implementation pass.
60
+
61
+ If a new feature causes an existing test to fail:
62
+
63
+ 1. Determine whether the existing behavior is actually incorrect.
64
+ 2. Determine whether the new implementation introduced a regression.
65
+ 3. Fix the implementation when the regression is real.
66
+ 4. Change an existing test only when the intended public behavior has genuinely changed.
67
+
68
+ ### Google API Tests
69
+
70
+ Tests that communicate with the real Google Sheets API should remain separate from normal driver tests.
71
+
72
+ Real API tests require appropriate Google credentials and a dedicated test spreadsheet.
73
+
74
+ Do not commit:
75
+
76
+ - service-account private keys
77
+ - credential JSON files
78
+ - spreadsheet secrets
79
+ - environment files containing credentials
80
+ - production spreadsheet identifiers when they are sensitive
81
+
82
+ ### Pull Requests
83
+
84
+ A pull request should clearly describe:
85
+
86
+ - what changed
87
+ - why the change is required
88
+ - which layer was modified
89
+ - which tests were added or changed
90
+ - how the change was verified
91
+ - whether documentation was updated
92
+
93
+ Keep pull requests focused.
94
+
95
+ Unrelated formatting changes, refactoring, or dependency changes should not be mixed into a feature or bug-fix contribution unless they are necessary.
96
+
97
+ ### Verification
98
+
99
+ Before submitting a contribution, run:
100
+
101
+ ```bash id="8k4m2p"
102
+ bun run typecheck
103
+ bun test
104
+ ```
105
+
106
+ When relevant, also run the specific integration or performance test:
107
+
108
+ ```bash id="5v9n1x"
109
+ bun test test/google-sheets/0001-client.test.ts
110
+ bun test test/google-sheets/0007-performance.test.ts
111
+ ```
112
+
113
+ All relevant tests should pass before the contribution is submitted.
114
+
115
+ ### Documentation
116
+
117
+ If a contribution changes supported behavior, update the appropriate documentation.
118
+
119
+ Examples include:
120
+
121
+ - adding a new supported TypeORM feature
122
+ - changing configuration behavior
123
+ - adding a new limitation
124
+ - changing authentication requirements
125
+ - changing performance characteristics
126
+ - adding or removing Google Sheets API capabilities
127
+
128
+ Documentation should describe behavior that is actually implemented and tested.
129
+
130
+ ### Unsupported Features
131
+
132
+ Do not silently emulate unsupported relational-database behavior.
133
+
134
+ For example, Google Sheets does not provide database transactions or database-level foreign-key enforcement.
135
+
136
+ If a TypeORM feature cannot be meaningfully supported by Google Sheets, it should either remain unsupported or be explicitly documented as a driver limitation.
137
+
138
+ ### Code Quality
139
+
140
+ Contributions should:
141
+
142
+ - use TypeScript consistently
143
+ - preserve the existing ESM module structure
144
+ - follow the existing naming conventions
145
+ - avoid unnecessary abstractions
146
+ - keep error handling explicit
147
+ - avoid unnecessary production changes
148
+ - maintain the separation between the driver and Google Sheets API client
149
+
150
+ ### Contribution Checklist
151
+
152
+ ```text
153
+ [ ] The affected layer has been identified
154
+ [ ] Existing behavior and tests have been reviewed
155
+ [ ] Focused tests cover the change
156
+ [ ] No unrelated tests were modified
157
+ [ ] TypeScript typecheck passes
158
+ [ ] Full test suite passes
159
+ [ ] Integration tests pass when applicable
160
+ [ ] Performance impact has been considered when applicable
161
+ [ ] Documentation has been updated when necessary
162
+ [ ] No credentials or sensitive data are included
163
+ [ ] The contribution is focused and minimal
164
+ ```
165
+
166
+ ### Summary
167
+
168
+ The goal of contributions is not simply to make a feature work, but to maintain a stable TypeORM-compatible driver with predictable Google Sheets behavior.
169
+
170
+ Contributors should prioritize:
171
+
172
+ - compatibility
173
+ - tested behavior
174
+ - minimal changes
175
+ - clear separation of concerns
176
+ - explicit limitations
177
+ - maintainable documentation
package/docs/crud.md ADDED
@@ -0,0 +1,351 @@
1
+ ## CRUD Examples
2
+
3
+ `tantan-typeorm-gs` exposes the standard TypeORM repository API for supported CRUD operations.
4
+
5
+ The examples below assume the following entity:
6
+
7
+ ```ts
8
+ import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
9
+
10
+ @Entity("users")
11
+ class User {
12
+ @PrimaryGeneratedColumn()
13
+ id!: number;
14
+
15
+ @Column()
16
+ name!: string;
17
+
18
+ @Column()
19
+ email!: string;
20
+ }
21
+ ```
22
+
23
+ After initializing the data source:
24
+
25
+ ```ts
26
+ await dataSource.initialize();
27
+
28
+ const repository = dataSource.getRepository(User);
29
+ ```
30
+
31
+ ### Create
32
+
33
+ #### Insert One Row
34
+
35
+ Use `repository.insert()` to insert a new row:
36
+
37
+ ```ts
38
+ const result = await repository.insert({
39
+ name: "Budi",
40
+ email: "budi@example.com"
41
+ });
42
+ ```
43
+
44
+ For an entity with an auto-generated primary key, the driver generates the ID automatically.
45
+
46
+ #### Insert Multiple Rows
47
+
48
+ Multiple rows can be inserted in a single repository operation:
49
+
50
+ ```ts
51
+ await repository.insert([
52
+ {
53
+ name: "Budi",
54
+ email: "budi@example.com"
55
+ },
56
+ {
57
+ name: "Siti",
58
+ email: "siti@example.com"
59
+ },
60
+ {
61
+ name: "Andi",
62
+ email: "andi@example.com"
63
+ }
64
+ ]);
65
+ ```
66
+
67
+ The driver processes the rows as a batch operation.
68
+
69
+ #### Save
70
+
71
+ `repository.save()` can also be used:
72
+
73
+ ```ts
74
+ const user = repository.create({
75
+ name: "Budi",
76
+ email: "budi@example.com"
77
+ });
78
+
79
+ await repository.save(user);
80
+ ```
81
+
82
+ `save()` follows TypeORM's repository semantics for determining whether the entity should be inserted or updated.
83
+
84
+ ### Read
85
+
86
+ #### Find All Rows
87
+
88
+ Use `repository.find()` to retrieve all rows:
89
+
90
+ ```ts
91
+ const users = await repository.find();
92
+ ```
93
+
94
+ The result is an array of entity instances:
95
+
96
+ ```ts
97
+ for (const user of users) {
98
+ console.log(user.id, user.name, user.email);
99
+ }
100
+ ```
101
+
102
+ #### Find One Row
103
+
104
+ A specific row can be retrieved using `findOne()`:
105
+
106
+ ```ts
107
+ const user = await repository.findOne({
108
+ where: {
109
+ id: 1
110
+ }
111
+ });
112
+ ```
113
+
114
+ If no matching row exists, the result is `null`.
115
+
116
+ ```ts
117
+ if (!user) {
118
+ console.log("User not found.");
119
+ }
120
+ ```
121
+
122
+ #### Find by Conditions
123
+
124
+ Multiple conditions can be supplied:
125
+
126
+ ```ts
127
+ const users = await repository.find({
128
+ where: {
129
+ name: "Budi"
130
+ }
131
+ });
132
+ ```
133
+
134
+ The available filtering capabilities are covered in the **Find Options** section.
135
+
136
+ ### Update
137
+
138
+ Use `repository.update()` to update rows matching a condition:
139
+
140
+ ```ts
141
+ await repository.update(
142
+ {
143
+ id: 1
144
+ },
145
+ {
146
+ name: "Budi Updated"
147
+ }
148
+ );
149
+ ```
150
+
151
+ The first argument specifies which rows should be updated.
152
+
153
+ The second argument contains the values to update.
154
+
155
+ Multiple rows can be affected when the condition matches multiple records.
156
+
157
+ ### Delete
158
+
159
+ Use `repository.delete()` to delete rows matching a condition:
160
+
161
+ ```ts
162
+ await repository.delete({
163
+ id: 1
164
+ });
165
+ ```
166
+
167
+ The operation removes the matching row from the worksheet.
168
+
169
+ A condition should be supplied when deleting data to avoid unintentionally deleting more rows than intended.
170
+
171
+ ### Remove Entity
172
+
173
+ An entity instance can also be removed using `repository.remove()`:
174
+
175
+ ```ts
176
+ const user = await repository.findOne({
177
+ where: {
178
+ id: 1
179
+ }
180
+ });
181
+
182
+ if (user) {
183
+ await repository.remove(user);
184
+ }
185
+ ```
186
+
187
+ `remove()` operates on an entity instance, while `delete()` operates directly from a deletion condition.
188
+
189
+ ### Update Using an Entity
190
+
191
+ An existing entity can be modified and persisted using `save()`:
192
+
193
+ ```ts
194
+ const user = await repository.findOne({
195
+ where: {
196
+ id: 1
197
+ }
198
+ });
199
+
200
+ if (user) {
201
+ user.name = "Budi Updated";
202
+
203
+ await repository.save(user);
204
+ }
205
+ ```
206
+
207
+ This is useful when the application already has the entity instance and wants TypeORM to persist its changes.
208
+
209
+ ### Generated IDs
210
+
211
+ For an entity using:
212
+
213
+ ```ts
214
+ @PrimaryGeneratedColumn()
215
+ id!: number;
216
+ ```
217
+
218
+ the ID does not need to be supplied when creating a new row:
219
+
220
+ ```ts
221
+ await repository.insert({
222
+ name: "Budi",
223
+ email: "budi@example.com"
224
+ });
225
+ ```
226
+
227
+ The driver assigns the generated primary key.
228
+
229
+ An explicitly supplied generated ID is also preserved:
230
+
231
+ ```ts
232
+ await repository.insert({
233
+ id: 100,
234
+ name: "Budi",
235
+ email: "budi@example.com"
236
+ });
237
+ ```
238
+
239
+ If the supplied primary key already exists, the driver rejects the operation as a duplicate primary-key insert.
240
+
241
+ ### Manually Assigned Primary Keys
242
+
243
+ For an entity using:
244
+
245
+ ```ts
246
+ @PrimaryColumn()
247
+ id!: number;
248
+ ```
249
+
250
+ the primary key must be supplied:
251
+
252
+ ```ts
253
+ await repository.insert({
254
+ id: 1,
255
+ name: "Budi",
256
+ email: "budi@example.com"
257
+ });
258
+ ```
259
+
260
+ An insert without the required primary key is rejected.
261
+
262
+ ### Lifecycle Hooks
263
+
264
+ CRUD operations participate in supported TypeORM entity lifecycle events.
265
+
266
+ For example:
267
+
268
+ ```ts
269
+ @BeforeInsert()
270
+ beforeInsert()
271
+ {
272
+ this.name =
273
+ this.name.toUpperCase();
274
+ }
275
+ ```
276
+
277
+ When the entity is inserted, the lifecycle hook is executed as part of the TypeORM operation.
278
+
279
+ Subscribers are also supported for the corresponding lifecycle events.
280
+
281
+ See the **Supported Features** section for the supported lifecycle behavior.
282
+
283
+ ### Complete Example
284
+
285
+ A simple application can perform a complete CRUD flow:
286
+
287
+ ```ts
288
+ const dataSource = createGoogleSheetsDataSource({
289
+ type: "google-sheets",
290
+
291
+ spreadsheetId: process.env.GOOGLE_SHEETS_SPREADSHEET_ID!,
292
+
293
+ credentials: {
294
+ clientEmail: process.env.GOOGLE_SHEETS_CLIENT_EMAIL!,
295
+
296
+ privateKey: process.env.GOOGLE_SHEETS_PRIVATE_KEY!
297
+ },
298
+
299
+ entities: [User]
300
+ });
301
+
302
+ await dataSource.initialize();
303
+
304
+ const repository = dataSource.getRepository(User);
305
+
306
+ // Create
307
+ const user = await repository.save({
308
+ name: "Budi",
309
+ email: "budi@example.com"
310
+ });
311
+
312
+ // Read
313
+ const found = await repository.findOne({
314
+ where: {
315
+ id: user.id
316
+ }
317
+ });
318
+
319
+ // Update
320
+ await repository.update(
321
+ {
322
+ id: user.id
323
+ },
324
+ {
325
+ name: "Budi Updated"
326
+ }
327
+ );
328
+
329
+ // Delete
330
+ await repository.delete({
331
+ id: user.id
332
+ });
333
+
334
+ await dataSource.destroy();
335
+ ```
336
+
337
+ ### CRUD Mapping
338
+
339
+ The common repository operations can be summarized as:
340
+
341
+ | Operation | Repository API |
342
+ | ------------------- | --------------------- |
343
+ | Create one | `insert()` / `save()` |
344
+ | Create multiple | `insert()` |
345
+ | Read multiple | `find()` |
346
+ | Read one | `findOne()` |
347
+ | Update | `update()` / `save()` |
348
+ | Delete by condition | `delete()` |
349
+ | Delete entity | `remove()` |
350
+
351
+ These APIs operate through the Google Sheets driver and are subject to the driver's supported TypeORM behavior and Google Sheets limitations.
@@ -0,0 +1,165 @@
1
+ ## Custom Client
2
+
3
+ The Google Sheets driver allows applications to provide a custom `GoogleSheetsClient`.
4
+
5
+ This is useful when the application needs to control how Google Sheets data is accessed, for example for testing, mocking, caching, or implementing a custom integration.
6
+
7
+ ### Default Client
8
+
9
+ If no custom client is provided, the data source creates its own `GoogleSheetsApiClient` using the configured spreadsheet ID and credentials.
10
+
11
+ ```ts
12
+ const dataSource = createGoogleSheetsDataSource({
13
+ type: "google-sheets",
14
+
15
+ spreadsheetId: process.env.GOOGLE_SHEETS_SPREADSHEET_ID!,
16
+
17
+ credentials: {
18
+ clientEmail: process.env.GOOGLE_SHEETS_CLIENT_EMAIL!,
19
+
20
+ privateKey: process.env.GOOGLE_SHEETS_PRIVATE_KEY!
21
+ },
22
+
23
+ entities: [User]
24
+ });
25
+ ```
26
+
27
+ The default flow is:
28
+
29
+ ```text
30
+ createGoogleSheetsDataSource()
31
+ ↓
32
+ client not provided
33
+ ↓
34
+ GoogleSheetsApiClient
35
+ ↓
36
+ Google Sheets API
37
+ ```
38
+
39
+ ### Providing a Custom Client
40
+
41
+ A custom client can be supplied through the `client` option:
42
+
43
+ ```ts
44
+ const client = new MyGoogleSheetsClient();
45
+
46
+ const dataSource = createGoogleSheetsDataSource({
47
+ type: "google-sheets",
48
+
49
+ spreadsheetId: process.env.GOOGLE_SHEETS_SPREADSHEET_ID!,
50
+
51
+ credentials: {
52
+ clientEmail: process.env.GOOGLE_SHEETS_CLIENT_EMAIL!,
53
+
54
+ privateKey: process.env.GOOGLE_SHEETS_PRIVATE_KEY!
55
+ },
56
+
57
+ client,
58
+
59
+ entities: [User]
60
+ });
61
+ ```
62
+
63
+ When `client` is provided, the driver uses that client instead of creating a default `GoogleSheetsApiClient`.
64
+
65
+ ```text
66
+ createGoogleSheetsDataSource()
67
+ ↓
68
+ client provided?
69
+ ┌────┴────┐
70
+ yes no
71
+ ↓ ↓
72
+ custom GoogleSheetsApiClient
73
+ client
74
+ ↓ ↓
75
+ └────┬────┘
76
+ ↓
77
+ GoogleSheetsDriver
78
+ ```
79
+
80
+ ### Custom Client Interface
81
+
82
+ A custom client must implement the `GoogleSheetsClient` interface expected by the driver.
83
+
84
+ For example:
85
+
86
+ ```ts
87
+ import type { GoogleSheetsClient } from "tantan-typeorm-gs";
88
+
89
+ class MyGoogleSheetsClient implements GoogleSheetsClient {
90
+ // Implement the required client methods.
91
+ }
92
+ ```
93
+
94
+ The exact methods depend on the operations used by the driver.
95
+
96
+ A custom client therefore allows the driver layer to remain independent from the actual Google Sheets transport implementation.
97
+
98
+ ### Testing with a Fake Client
99
+
100
+ A custom client is particularly useful for tests.
101
+
102
+ For example:
103
+
104
+ ```ts
105
+ const client = new Memory({
106
+ users: [
107
+ {
108
+ id: 1,
109
+ name: "Budi"
110
+ }
111
+ ]
112
+ });
113
+
114
+ const dataSource = createGoogleSheetsDataSource({
115
+ type: "google-sheets",
116
+
117
+ client,
118
+
119
+ entities: [User]
120
+ });
121
+
122
+ await dataSource.initialize();
123
+
124
+ const repository = dataSource.getRepository(User);
125
+
126
+ const users = await repository.find();
127
+ ```
128
+
129
+ This allows repository and driver behavior to be tested without making requests to the Google Sheets API.
130
+
131
+ ### Custom Client Precedence
132
+
133
+ When `client` is supplied:
134
+
135
+ ```ts
136
+ client;
137
+ ```
138
+
139
+ takes precedence over the default API client.
140
+
141
+ The credentials and spreadsheet ID are still part of the data source configuration, but the custom client is the client instance used by the driver.
142
+
143
+ ### When to Use a Custom Client
144
+
145
+ A custom client is useful for:
146
+
147
+ - unit and integration tests
148
+ - fake or in-memory spreadsheet data
149
+ - custom authentication handling
150
+ - request logging
151
+ - caching
152
+ - retry policies
153
+ - alternative Google Sheets transport implementations
154
+
155
+ For normal production usage, the default `GoogleSheetsApiClient` is sufficient when direct Google Sheets API access is required.
156
+
157
+ ### Summary
158
+
159
+ | Configuration | Client used |
160
+ | -------------------------------- | ---------------------------------------------- |
161
+ | `client` omitted | `GoogleSheetsApiClient` |
162
+ | `client` provided | Provided `GoogleSheetsClient` |
163
+ | `client` provided with fake data | Useful for testing without Google API requests |
164
+
165
+ The custom client is injected at data source creation time and is used by the `GoogleSheetsDriver`.