@gasboost/query 0.1.0 → 0.1.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.
- package/README.md +408 -0
- package/dist/FilterOperand.js +12 -6
- package/dist/index.d.ts +7 -0
- package/dist/index.js +22 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# @gasboost/query
|
|
2
|
+
|
|
3
|
+
Zod Schema を利用した、型安全でストレージ非依存の Query Engine です。
|
|
4
|
+
|
|
5
|
+
`@gasboost/query` は Query の構築、Filter、Sort、Offset、Limit、JOIN、および再帰的な JOIN 解決を提供します。
|
|
6
|
+
|
|
7
|
+
Google Sheets、IndexedDB、その他のデータストアには依存せず、テーブルから Record を取得する `Loader` を渡すことで同じ Query を異なるストレージに対して利用できます。
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
@gasboost/query
|
|
11
|
+
↓
|
|
12
|
+
Query / Join
|
|
13
|
+
↓
|
|
14
|
+
Loader
|
|
15
|
+
↙ ↘
|
|
16
|
+
SheetORM Replica
|
|
17
|
+
↓ ↓
|
|
18
|
+
Sheets IndexedDB
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- Zod Schema による型安全な Query
|
|
26
|
+
- 型安全な Table 選択
|
|
27
|
+
- 型安全な Column / Operand
|
|
28
|
+
- AND / OR Filter
|
|
29
|
+
- Order By
|
|
30
|
+
- Limit / Offset
|
|
31
|
+
- JOIN
|
|
32
|
+
- Nested JOIN
|
|
33
|
+
- Recursive Query Resolution
|
|
34
|
+
- Storage Agnostic
|
|
35
|
+
- Async Loader
|
|
36
|
+
- Google Apps Script / Dexie / IndexedDB 非依存
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
# Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm add @gasboost/query zod
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
npm の場合:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install @gasboost/query zod
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
# Table Definition
|
|
55
|
+
|
|
56
|
+
Query が必要とする Table 定義は `name` と Zod Schema だけです。
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { z } from "zod";
|
|
60
|
+
import type { TableDefinition } from "@gasboost/query";
|
|
61
|
+
|
|
62
|
+
const UserSchema = z.object({
|
|
63
|
+
id: z.string(),
|
|
64
|
+
name: z.string(),
|
|
65
|
+
age: z.number(),
|
|
66
|
+
active: z.boolean(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const ReservationSchema = z.object({
|
|
70
|
+
id: z.string(),
|
|
71
|
+
userId: z.string(),
|
|
72
|
+
staffId: z.string(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const StaffSchema = z.object({
|
|
76
|
+
id: z.string(),
|
|
77
|
+
name: z.string(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const tables = [
|
|
81
|
+
{
|
|
82
|
+
name: "users",
|
|
83
|
+
schema: UserSchema,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "reservations",
|
|
87
|
+
schema: ReservationSchema,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "staffs",
|
|
91
|
+
schema: StaffSchema,
|
|
92
|
+
},
|
|
93
|
+
] as const satisfies readonly TableDefinition[];
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`primaryKey`、Database ID、IndexedDB index などのストレージ固有情報は `TableDefinition` に含みません。
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
# Query
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { Query } from "@gasboost/query";
|
|
104
|
+
|
|
105
|
+
const query = new Query<typeof tables, "users">({
|
|
106
|
+
tableName: "users",
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Query の対象 Table は型として保持されます。
|
|
111
|
+
|
|
112
|
+
そのため、Query で利用できる Column は対象 Table の Zod Schema から推論されます。
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
# Filter
|
|
117
|
+
|
|
118
|
+
## AND
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const query = new Query<typeof tables, "users">({
|
|
122
|
+
tableName: "users",
|
|
123
|
+
})
|
|
124
|
+
.and("active", "=", [true])
|
|
125
|
+
.and("age", ">=", [20]);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
すべての AND 条件を満たす Record が対象になります。
|
|
129
|
+
|
|
130
|
+
## OR
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const query = new Query<typeof tables, "users">({
|
|
134
|
+
tableName: "users",
|
|
135
|
+
})
|
|
136
|
+
.or("name", "=", ["Alice"])
|
|
137
|
+
.or("name", "=", ["Bob"]);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
OR 条件のうち1つ以上を満たす Record が対象になります。
|
|
141
|
+
|
|
142
|
+
AND と OR は組み合わせられます。
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const query = new Query<typeof tables, "users">({
|
|
146
|
+
tableName: "users",
|
|
147
|
+
})
|
|
148
|
+
.and("active", "=", [true])
|
|
149
|
+
.or("name", "=", ["Alice"])
|
|
150
|
+
.or("name", "=", ["Bob"]);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
# Filter Operators
|
|
156
|
+
|
|
157
|
+
利用可能な Operator は以下です。
|
|
158
|
+
|
|
159
|
+
| Operator | 意味 |
|
|
160
|
+
| -------- | ------------------ |
|
|
161
|
+
| `=` | 等しい |
|
|
162
|
+
| `!=` | 等しくない |
|
|
163
|
+
| `<` | より小さい |
|
|
164
|
+
| `>` | より大きい |
|
|
165
|
+
| `<=` | 以下 |
|
|
166
|
+
| `>=` | 以上 |
|
|
167
|
+
| `*` | 文字列を含む |
|
|
168
|
+
| `!*` | 文字列を含まない |
|
|
169
|
+
| `^*` | 指定文字列で始まる |
|
|
170
|
+
| `*$` | 指定文字列で終わる |
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
query.and("age", ">=", [20]);
|
|
174
|
+
|
|
175
|
+
query.and("name", "*", ["Ali"]);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Operator と Operand の組み合わせは実行時にも検証されます。
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
# Order By
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const query = new Query<typeof tables, "users">({
|
|
186
|
+
tableName: "users",
|
|
187
|
+
}).orderBy("name", "asc");
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
降順:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
query.orderBy("name", "desc");
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
# Limit / Offset
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const query = new Query<typeof tables, "users">({
|
|
202
|
+
tableName: "users",
|
|
203
|
+
})
|
|
204
|
+
.offset(20)
|
|
205
|
+
.limit(10);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Query は次の順番で Record に適用されます。
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
Filter
|
|
212
|
+
↓
|
|
213
|
+
Order By
|
|
214
|
+
↓
|
|
215
|
+
Offset
|
|
216
|
+
↓
|
|
217
|
+
Limit
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
# Apply
|
|
223
|
+
|
|
224
|
+
取得済みの Record 配列へ Query を直接適用できます。
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
const result = query.apply(records);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`apply()` はストレージ I/O を行いません。
|
|
231
|
+
|
|
232
|
+
Record 配列に対する純粋な Query 処理のみを行います。
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
# JOIN
|
|
237
|
+
|
|
238
|
+
異なる Table の Record を型安全に JOIN できます。
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const query = new Query<typeof tables, "users">({
|
|
242
|
+
tableName: "users",
|
|
243
|
+
}).join("id", "reservations", "userId");
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
この Query は、
|
|
247
|
+
|
|
248
|
+
```text
|
|
249
|
+
users.id
|
|
250
|
+
↓
|
|
251
|
+
reservations.userId
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
で Record を関連付けます。
|
|
255
|
+
|
|
256
|
+
JOIN 結果は JOIN 先 Table 名をプロパティとして持ちます。
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
{
|
|
260
|
+
id: "user-1",
|
|
261
|
+
name: "Alice",
|
|
262
|
+
reservations: [
|
|
263
|
+
{
|
|
264
|
+
id: "reservation-1",
|
|
265
|
+
userId: "user-1",
|
|
266
|
+
staffId: "staff-1"
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`relations` などの特定の Record 表現には依存しません。
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
# Nested JOIN
|
|
277
|
+
|
|
278
|
+
JOIN 先には別の Query を指定できます。
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
const reservations = new Query<typeof tables, "reservations">({
|
|
282
|
+
tableName: "reservations",
|
|
283
|
+
}).join("staffId", "staffs", "id");
|
|
284
|
+
|
|
285
|
+
const users = new Query<typeof tables, "users">({
|
|
286
|
+
tableName: "users",
|
|
287
|
+
}).join("id", "reservations", "userId", reservations);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Query 自身が JOIN の木構造を保持します。
|
|
291
|
+
|
|
292
|
+
```text
|
|
293
|
+
users
|
|
294
|
+
└─ reservations
|
|
295
|
+
└─ staffs
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
そのため、Table 側に Relation Tree を持たせなくても Query から必要な JOIN 構造を決定できます。
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
# Resolve
|
|
303
|
+
|
|
304
|
+
`resolve()` に Loader を渡すことで、データ取得から Query 適用、再帰 JOIN までを一度に解決できます。
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
const result = await users.resolve(load);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Loader は Table 名を受け取り、その Table の Record を返します。
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
const load = async (table: string): Promise<Record<string, unknown>[]> => {
|
|
314
|
+
// 任意の storage から Record を取得
|
|
315
|
+
};
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Query は JOIN を再帰的に解決します。
|
|
319
|
+
|
|
320
|
+
```text
|
|
321
|
+
users
|
|
322
|
+
└─ reservations
|
|
323
|
+
└─ staffs
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
の場合、概念的には bottom-up に処理されます。
|
|
327
|
+
|
|
328
|
+
```text
|
|
329
|
+
staffs
|
|
330
|
+
↓ JOIN
|
|
331
|
+
reservations
|
|
332
|
+
↓ JOIN
|
|
333
|
+
users
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
これにより、深い JOIN を持つ Query でも呼び出し側では、
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
const result = await query.resolve(load);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
だけで解決できます。
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
# Storage Agnostic
|
|
347
|
+
|
|
348
|
+
`@gasboost/query` は Record の取得元を知りません。
|
|
349
|
+
|
|
350
|
+
例えば SheetORM では Google Sheets から取得できます。
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
const result = await query.resolve(async (table) => {
|
|
354
|
+
return sheetStorage.read(table);
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Replica では IndexedDB から取得できます。
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
const result = await query.resolve(async (table) => {
|
|
362
|
+
return replicaStorage.read(table);
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Query の意味とストレージ I/O を分離することで、同じ Query を複数のデータストアで共有できます。
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
# Architecture
|
|
371
|
+
|
|
372
|
+
`@gasboost/query` の責務は、Record に対する問い合わせ処理です。
|
|
373
|
+
|
|
374
|
+
```text
|
|
375
|
+
Query
|
|
376
|
+
├─ Filter
|
|
377
|
+
│ ├─ FilterOperator
|
|
378
|
+
│ └─ FilterOperand
|
|
379
|
+
├─ OrderBy
|
|
380
|
+
├─ Join
|
|
381
|
+
└─ resolve()
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
ストレージ固有の責務は外部に残します。
|
|
385
|
+
|
|
386
|
+
```text
|
|
387
|
+
@gasboost/query
|
|
388
|
+
- Filter
|
|
389
|
+
- Sort
|
|
390
|
+
- Offset
|
|
391
|
+
- Limit
|
|
392
|
+
- Join
|
|
393
|
+
- Recursive resolution
|
|
394
|
+
|
|
395
|
+
@gasboost/sheetorm
|
|
396
|
+
- Google Sheets I/O
|
|
397
|
+
|
|
398
|
+
@gasboost/replica
|
|
399
|
+
- IndexedDB I/O
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
この分離により、Query の評価ロジックを各ストレージ実装で重複させずに利用できます。
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
# License
|
|
407
|
+
|
|
408
|
+
MIT
|
package/dist/FilterOperand.js
CHANGED
|
@@ -5,18 +5,24 @@ class FilterOperand {
|
|
|
5
5
|
values;
|
|
6
6
|
constructor(values) {
|
|
7
7
|
this.values = values;
|
|
8
|
-
if (values.length === 0)
|
|
8
|
+
if (values.length === 0) {
|
|
9
9
|
throw new Error("values must not be empty");
|
|
10
|
-
|
|
11
|
-
if (
|
|
10
|
+
}
|
|
11
|
+
if (values[0] instanceof Date) {
|
|
12
|
+
const isSameType = values.every((value) => value instanceof Date);
|
|
13
|
+
if (!isSameType) {
|
|
14
|
+
throw new Error("values must be all the same type");
|
|
15
|
+
}
|
|
12
16
|
return;
|
|
13
|
-
|
|
17
|
+
}
|
|
18
|
+
const firstType = typeof values[0];
|
|
19
|
+
const isSameType = values.every((value) => value !== null && value !== undefined && typeof value === firstType);
|
|
14
20
|
if (!isSameType) {
|
|
15
21
|
throw new Error("values must be all the same type");
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
24
|
isDate() {
|
|
19
|
-
return
|
|
25
|
+
return this.values[0] instanceof Date;
|
|
20
26
|
}
|
|
21
27
|
getValue() {
|
|
22
28
|
return this.values;
|
|
@@ -25,7 +31,7 @@ class FilterOperand {
|
|
|
25
31
|
if (!this.isDate()) {
|
|
26
32
|
throw new Error("values are not Date type");
|
|
27
33
|
}
|
|
28
|
-
return this.values.map((
|
|
34
|
+
return this.values.map((value) => value.getTime());
|
|
29
35
|
}
|
|
30
36
|
isStringOrBoolean() {
|
|
31
37
|
return (typeof this.values[0] === "string" || typeof this.values[0] === "boolean");
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./Filter"), exports);
|
|
18
|
+
__exportStar(require("./FilterOperand"), exports);
|
|
19
|
+
__exportStar(require("./FilterOperator"), exports);
|
|
20
|
+
__exportStar(require("./Join"), exports);
|
|
21
|
+
__exportStar(require("./OrderBy"), exports);
|
|
22
|
+
__exportStar(require("./Query"), exports);
|
|
23
|
+
__exportStar(require("./TableDefinition"), exports);
|