pgsql-template-tag 0.0.4 → 0.0.6
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/dist/src/core.d.ts +43 -1
- package/dist/src/core.d.ts.map +1 -1
- package/dist/src/core.js +131 -25
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/sql.d.ts +30 -2
- package/dist/src/sql.d.ts.map +1 -1
- package/dist/src/sql.js +19 -2
- package/package.json +7 -4
- package/src/core.ts +174 -31
- package/src/index.ts +1 -1
- package/src/sql.ts +39 -2
package/dist/src/core.d.ts
CHANGED
|
@@ -5,9 +5,32 @@ export type Value = unknown;
|
|
|
5
5
|
/**
|
|
6
6
|
* SQL クエリーの構築に使用できる生の値、または別の Sql インスタンスを表す型です。
|
|
7
7
|
*/
|
|
8
|
-
export type RawValue = Value | Sql;
|
|
8
|
+
export type RawValue = Value | Slot | Sql;
|
|
9
|
+
/**
|
|
10
|
+
* スロットを表すクラスです。
|
|
11
|
+
*
|
|
12
|
+
* スロットは後から値を注入可能なプレースホルダーです。
|
|
13
|
+
*/
|
|
14
|
+
export declare class Slot {
|
|
15
|
+
/**
|
|
16
|
+
* スロット名です。
|
|
17
|
+
*/
|
|
18
|
+
readonly name: string;
|
|
19
|
+
/**
|
|
20
|
+
* デフォルト値です。
|
|
21
|
+
*/
|
|
22
|
+
readonly defaultValue: Value;
|
|
23
|
+
/**
|
|
24
|
+
* 新しい Slot インスタンスを初期化します。
|
|
25
|
+
*
|
|
26
|
+
* @param name スロット名です。
|
|
27
|
+
* @param defaultValue デフォルト値です。
|
|
28
|
+
*/
|
|
29
|
+
constructor(name: string, defaultValue?: Value);
|
|
30
|
+
}
|
|
9
31
|
/**
|
|
10
32
|
* 安全な SQL クエリーを構築するためのクラスです。
|
|
33
|
+
*
|
|
11
34
|
* プレースホルダーを使用したパラメーター化クエリーを生成します。
|
|
12
35
|
*/
|
|
13
36
|
export declare class Sql {
|
|
@@ -34,6 +57,15 @@ export declare class Sql {
|
|
|
34
57
|
* @param rawBindings 文字列の間に挿入される値の配列です。
|
|
35
58
|
*/
|
|
36
59
|
constructor(rawStrings: readonly string[], rawBindings: readonly RawValue[]);
|
|
60
|
+
/**
|
|
61
|
+
* スロットを値で埋めます。
|
|
62
|
+
*
|
|
63
|
+
* @param slots スロットの値です。
|
|
64
|
+
* @returns スロットが埋められた新しい Sql インスタンスです。
|
|
65
|
+
*/
|
|
66
|
+
fill(slots: {
|
|
67
|
+
readonly [name: string]: Value;
|
|
68
|
+
} | Iterable<readonly [string | Slot, Value]>): Sql;
|
|
37
69
|
/**
|
|
38
70
|
* オブジェクトを JSON 形式に変換可能な形式で返します。
|
|
39
71
|
*
|
|
@@ -52,6 +84,7 @@ export declare class Sql {
|
|
|
52
84
|
}
|
|
53
85
|
/**
|
|
54
86
|
* 生の文字列を SQL 断片として扱います。
|
|
87
|
+
*
|
|
55
88
|
* この値はパラメーター化の対象にならず、そのままクエリーに含まれます。
|
|
56
89
|
*
|
|
57
90
|
* @param value SQL に含める生の文字列です。
|
|
@@ -81,4 +114,13 @@ export declare function join(values: readonly RawValue[], separator?: string | u
|
|
|
81
114
|
* @returns エスケープ済みの識別子文字列を返します。
|
|
82
115
|
*/
|
|
83
116
|
export declare function ident(value: string): string;
|
|
117
|
+
/**
|
|
118
|
+
* 文字列を SQL のリテラル文字列として安全にエスケープします。
|
|
119
|
+
*
|
|
120
|
+
* 一重引用符を二重にすることでエスケープを行い、全体を一重引用符で囲みます。
|
|
121
|
+
*
|
|
122
|
+
* @param value エスケープする文字列です。
|
|
123
|
+
* @returns エスケープ済みのリテラル文字列を返します。
|
|
124
|
+
*/
|
|
125
|
+
export declare function literal(value: string): string;
|
|
84
126
|
//# sourceMappingURL=core.d.ts.map
|
package/dist/src/core.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,MAAM,KAAK,GAAG,OAAO,CAAC;AAE5B;;GAEG;AAEH,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,IAAI,GAAG,GAAG,CAAC;AAE1C;;;;GAIG;AACH,qBAAa,IAAI;IACf;;OAEG;IACH,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;OAEG;IACH,SAAgB,YAAY,EAAE,KAAK,CAAC;IAEpC;;;;;OAKG;gBACgB,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,KAAK;CAQtD;AAgCD;;;;GAIG;AACH,qBAAa,GAAG;IACd;;;;;;OAMG;IACH,IAAW,IAAI,IAAI,MAAM,CAexB;IAED;;OAEG;IACH,SAAgB,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAEzC;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAgB;IAElC;;;;;OAKG;gBACgB,UAAU,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,EAAE,SAAS,QAAQ,EAAE;IA2GlF;;;;;OAKG;IACI,IAAI,CACT,KAAK,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,GAAG,QAAQ,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,EAAE,KAAK,CAAC,CAAC,GACpF,GAAG;IAmCN;;;;OAIG;IACI,MAAM,IAAI;QACf,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,KAAK,EAAE,CAAC;KACjB;IAOD;;;;OAIG;IACI,QAAQ,IAAI,MAAM;CAG1B;AAED;;;;;;;GAOG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAEtC;AAED;;GAEG;AACH,eAAO,MAAM,KAAK,EAAE,GAAa,CAAC;AAElC;;;;;;;;GAQG;AACH,wBAAgB,IAAI,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,EAAE,SAAS,GAAE,MAAM,GAAG,SAAe,GAAG,GAAG,CAM1F;AAOD;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3C;AAOD;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7C"}
|
package/dist/src/core.js
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
|
+
import { isPlainObject } from "es-toolkit/predicate";
|
|
2
|
+
/**
|
|
3
|
+
* スロットを表すクラスです。
|
|
4
|
+
*
|
|
5
|
+
* スロットは後から値を注入可能なプレースホルダーです。
|
|
6
|
+
*/
|
|
7
|
+
export class Slot {
|
|
8
|
+
/**
|
|
9
|
+
* 新しい Slot インスタンスを初期化します。
|
|
10
|
+
*
|
|
11
|
+
* @param name スロット名です。
|
|
12
|
+
* @param defaultValue デフォルト値です。
|
|
13
|
+
*/
|
|
14
|
+
constructor(name, defaultValue) {
|
|
15
|
+
if (arguments.length < 2) {
|
|
16
|
+
defaultValue = null;
|
|
17
|
+
}
|
|
18
|
+
this.name = name;
|
|
19
|
+
this.defaultValue = defaultValue;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
1
22
|
/**
|
|
2
23
|
* 安全な SQL クエリーを構築するためのクラスです。
|
|
24
|
+
*
|
|
3
25
|
* プレースホルダーを使用したパラメーター化クエリーを生成します。
|
|
4
26
|
*/
|
|
5
27
|
export class Sql {
|
|
@@ -12,15 +34,15 @@ export class Sql {
|
|
|
12
34
|
*/
|
|
13
35
|
get text() {
|
|
14
36
|
// キャッシュが存在しない場合にのみ、文字列を構築します。
|
|
15
|
-
if (this._.
|
|
16
|
-
let i = 0, text = this._.
|
|
37
|
+
if (this._.text === undefined) {
|
|
38
|
+
let i = 0, text = this._.parts[0];
|
|
17
39
|
// 文字列の断片とプレースホルダー($1, $2...)を交互に結合します。
|
|
18
|
-
for (; i < this._.
|
|
19
|
-
text += "$" + this._.
|
|
40
|
+
for (; i < this._.phIds.length; i++) {
|
|
41
|
+
text += "$" + this._.phIds[i] + this._.parts[i + 1];
|
|
20
42
|
}
|
|
21
|
-
this._.
|
|
43
|
+
this._.text = text;
|
|
22
44
|
}
|
|
23
|
-
return this._.
|
|
45
|
+
return this._.text;
|
|
24
46
|
}
|
|
25
47
|
/**
|
|
26
48
|
* 新しい Sql インスタンスを初期化します。
|
|
@@ -38,8 +60,44 @@ export class Sql {
|
|
|
38
60
|
const strings = [rawStrings[0]];
|
|
39
61
|
const bindings = [];
|
|
40
62
|
const placeholderIds = [];
|
|
63
|
+
const idx2slot = new Map();
|
|
64
|
+
const slot2idx = new Map();
|
|
41
65
|
/** 値の重複を排除し、同じ値には同じプレースホルダー ID を割り当てるためのマップです。 */
|
|
42
66
|
const valueToId = new Map();
|
|
67
|
+
/** スロットごとのプレースホルダー ID を管理します。 */
|
|
68
|
+
const slotToId = new Map();
|
|
69
|
+
/**
|
|
70
|
+
* 値を bindings に登録し、placeholderId を取得します。
|
|
71
|
+
*/
|
|
72
|
+
const registerValue = (value) => {
|
|
73
|
+
let placeholderId = valueToId.get(value);
|
|
74
|
+
if (placeholderId === undefined) {
|
|
75
|
+
bindings.push(value);
|
|
76
|
+
placeholderId = bindings.length;
|
|
77
|
+
valueToId.set(value, placeholderId);
|
|
78
|
+
}
|
|
79
|
+
return placeholderId;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* スロットを bindings に登録し、placeholderId を取得します。
|
|
83
|
+
*/
|
|
84
|
+
const registerSlot = (slot) => {
|
|
85
|
+
let placeholderId = slotToId.get(slot);
|
|
86
|
+
if (placeholderId === undefined) {
|
|
87
|
+
bindings.push(slot.defaultValue);
|
|
88
|
+
placeholderId = bindings.length;
|
|
89
|
+
slotToId.set(slot, placeholderId);
|
|
90
|
+
const index = placeholderId - 1;
|
|
91
|
+
idx2slot.set(index, slot);
|
|
92
|
+
let idxes = slot2idx.get(slot.name);
|
|
93
|
+
if (idxes === undefined) {
|
|
94
|
+
idxes = new Set();
|
|
95
|
+
slot2idx.set(slot.name, idxes);
|
|
96
|
+
}
|
|
97
|
+
idxes.add(index);
|
|
98
|
+
}
|
|
99
|
+
return placeholderId;
|
|
100
|
+
};
|
|
43
101
|
// 提供された全てのバインディング値を走査して、SQL 文字列と値を正規化します。
|
|
44
102
|
for (let i = 0; i < rawBindings.length; i++) {
|
|
45
103
|
const child = rawBindings[i];
|
|
@@ -47,30 +105,22 @@ export class Sql {
|
|
|
47
105
|
// バインディング値が Sql インスタンス(ネストされたクエリー)の場合の処理です。
|
|
48
106
|
if (child instanceof Sql) {
|
|
49
107
|
// 現在の最後の文字列断片に、ネストされた Sql の最初の断片を結合します。
|
|
50
|
-
strings[strings.length - 1] += child._.
|
|
108
|
+
strings[strings.length - 1] += child._.parts[0];
|
|
51
109
|
// ネストされた Sql のプレースホルダーと値を再マッピングします。
|
|
52
|
-
for (let j = 0; j < child._.
|
|
53
|
-
const childPlaceholderId = child._.
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
valueToId.set(value, placeholderId);
|
|
60
|
-
}
|
|
61
|
-
strings.push(child._.s[j + 1]);
|
|
110
|
+
for (let j = 0; j < child._.phIds.length; j++) {
|
|
111
|
+
const childPlaceholderId = child._.phIds[j];
|
|
112
|
+
const valueIndex = childPlaceholderId - 1;
|
|
113
|
+
const value = child.values[valueIndex];
|
|
114
|
+
const slot = child._.idx2slot.get(valueIndex);
|
|
115
|
+
const placeholderId = slot !== undefined ? registerSlot(slot) : registerValue(value);
|
|
116
|
+
strings.push(child._.parts[j + 1]);
|
|
62
117
|
placeholderIds.push(placeholderId);
|
|
63
118
|
}
|
|
64
119
|
// ネストされた Sql の展開が終わった後に、後続の生の文字列を結合します。
|
|
65
120
|
strings[strings.length - 1] += rawString;
|
|
66
121
|
}
|
|
67
122
|
else {
|
|
68
|
-
|
|
69
|
-
if (placeholderId === undefined) {
|
|
70
|
-
bindings.push(child);
|
|
71
|
-
placeholderId = bindings.length;
|
|
72
|
-
valueToId.set(child, placeholderId);
|
|
73
|
-
}
|
|
123
|
+
const placeholderId = child instanceof Slot ? registerSlot(child) : registerValue(child);
|
|
74
124
|
strings.push(rawString);
|
|
75
125
|
placeholderIds.push(placeholderId);
|
|
76
126
|
}
|
|
@@ -79,11 +129,51 @@ export class Sql {
|
|
|
79
129
|
// 内部状態を隠蔽し、不必要なプロパティーの露出を防ぎます。
|
|
80
130
|
Object.defineProperty(this, "_", {
|
|
81
131
|
value: {
|
|
82
|
-
|
|
83
|
-
|
|
132
|
+
parts: strings,
|
|
133
|
+
phIds: placeholderIds,
|
|
134
|
+
idx2slot,
|
|
135
|
+
slot2idx,
|
|
84
136
|
},
|
|
85
137
|
});
|
|
86
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* スロットを値で埋めます。
|
|
141
|
+
*
|
|
142
|
+
* @param slots スロットの値です。
|
|
143
|
+
* @returns スロットが埋められた新しい Sql インスタンスです。
|
|
144
|
+
*/
|
|
145
|
+
fill(slots) {
|
|
146
|
+
if (isPlainObject(slots)) {
|
|
147
|
+
slots = Object.entries(slots);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// 一旦 Map のインスタンスにすることで、重複する名前またはスロットを 1 つに絞ります。
|
|
151
|
+
slots = new Map(slots);
|
|
152
|
+
}
|
|
153
|
+
const values = [...this.values];
|
|
154
|
+
for (const [target, value] of new Map(slots)) {
|
|
155
|
+
let idxes;
|
|
156
|
+
if (typeof target === "string") {
|
|
157
|
+
idxes = this._.slot2idx.get(target);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
for (const [idx, slot] of this._.idx2slot) {
|
|
161
|
+
if (slot === target) {
|
|
162
|
+
idxes || (idxes = new Set());
|
|
163
|
+
idxes.add(idx);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (idxes === undefined) {
|
|
168
|
+
// スロットが見つからない場合は無視します。
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
for (const idx of idxes) {
|
|
172
|
+
values[idx] = value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return new Sql(this._.parts, values);
|
|
176
|
+
}
|
|
87
177
|
/**
|
|
88
178
|
* オブジェクトを JSON 形式に変換可能な形式で返します。
|
|
89
179
|
*
|
|
@@ -106,6 +196,7 @@ export class Sql {
|
|
|
106
196
|
}
|
|
107
197
|
/**
|
|
108
198
|
* 生の文字列を SQL 断片として扱います。
|
|
199
|
+
*
|
|
109
200
|
* この値はパラメーター化の対象にならず、そのままクエリーに含まれます。
|
|
110
201
|
*
|
|
111
202
|
* @param value SQL に含める生の文字列です。
|
|
@@ -148,3 +239,18 @@ const DOUBLE_QUOTE_REGEX = /"/g;
|
|
|
148
239
|
export function ident(value) {
|
|
149
240
|
return '"' + value.replace(DOUBLE_QUOTE_REGEX, '""') + '"';
|
|
150
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* 一重引用符をエスケープするための正規表現です。
|
|
244
|
+
*/
|
|
245
|
+
const SINGLE_QUOTE_REGEX = /'/g;
|
|
246
|
+
/**
|
|
247
|
+
* 文字列を SQL のリテラル文字列として安全にエスケープします。
|
|
248
|
+
*
|
|
249
|
+
* 一重引用符を二重にすることでエスケープを行い、全体を一重引用符で囲みます。
|
|
250
|
+
*
|
|
251
|
+
* @param value エスケープする文字列です。
|
|
252
|
+
* @returns エスケープ済みのリテラル文字列を返します。
|
|
253
|
+
*/
|
|
254
|
+
export function literal(value) {
|
|
255
|
+
return "'" + value.replace(SINGLE_QUOTE_REGEX, "''") + "'";
|
|
256
|
+
}
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAE1F,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/src/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { empty, join, raw, ident
|
|
1
|
+
export { Sql, Slot, empty, join, raw, ident } from "./core.js";
|
|
2
2
|
export { sql } from "./sql.js";
|
package/dist/src/sql.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, raw, Sql, ident } from "./core.js";
|
|
1
|
+
import { join, raw, Sql, ident, literal, Slot } from "./core.js";
|
|
2
2
|
declare namespace sql {
|
|
3
3
|
/**
|
|
4
4
|
* SQL クエリーの構築に使用できる生の値、または別の Sql インスタンスを表す型です。
|
|
@@ -8,11 +8,25 @@ declare namespace sql {
|
|
|
8
8
|
* SQL クエリー内で使用される値の型定義です。
|
|
9
9
|
*/
|
|
10
10
|
type Value = import("./core.js").Value;
|
|
11
|
+
/**
|
|
12
|
+
* スロットを表すクラスです。
|
|
13
|
+
*
|
|
14
|
+
* スロットは後から値を注入可能なプレースホルダーです。
|
|
15
|
+
*/
|
|
16
|
+
type Slot = import("./core.js").Slot;
|
|
11
17
|
/**
|
|
12
18
|
* 安全な SQL クエリーを構築するためのクラス型です。
|
|
13
19
|
*/
|
|
14
20
|
type Sql = import("./core.js").Sql;
|
|
15
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* 新しい Slot インスタンスを作成します。
|
|
24
|
+
*
|
|
25
|
+
* @param name スロット名です。
|
|
26
|
+
* @param defaultValue デフォルト値です。
|
|
27
|
+
* @returns 作成された新しい Slot インスタンスです。
|
|
28
|
+
*/
|
|
29
|
+
declare function slot(name: string, defaultValue?: sql.Value): sql.Slot;
|
|
16
30
|
/**
|
|
17
31
|
* テンプレートリテラルを使用して SQL クエリーを安全に構築するためのタグ関数です。
|
|
18
32
|
*
|
|
@@ -26,13 +40,23 @@ declare namespace sql {
|
|
|
26
40
|
*/
|
|
27
41
|
declare const sql: ((strings: TemplateStringsArray, ...bindings: readonly sql.RawValue[]) => sql.Sql) & {
|
|
28
42
|
/**
|
|
29
|
-
* Sql
|
|
43
|
+
* Sql クラスです。
|
|
30
44
|
*/
|
|
31
45
|
readonly Sql: typeof Sql;
|
|
32
46
|
/**
|
|
33
47
|
* 生の文字列を SQL 断片として扱うための関数です。
|
|
34
48
|
*/
|
|
35
49
|
readonly raw: typeof raw;
|
|
50
|
+
/**
|
|
51
|
+
* 新しい Slot インスタンスを作成する関数です。
|
|
52
|
+
*/
|
|
53
|
+
readonly slot: typeof slot;
|
|
54
|
+
/**
|
|
55
|
+
* スロットを表すクラスです。
|
|
56
|
+
*
|
|
57
|
+
* スロットは後から値を注入可能なプレースホルダーです。
|
|
58
|
+
*/
|
|
59
|
+
readonly Slot: typeof Slot;
|
|
36
60
|
/**
|
|
37
61
|
* 複数の SQL 断片を結合するための関数です。
|
|
38
62
|
*/
|
|
@@ -45,6 +69,10 @@ declare const sql: ((strings: TemplateStringsArray, ...bindings: readonly sql.Ra
|
|
|
45
69
|
* 識別子(テーブル名等)を安全にエスケープするための関数です。
|
|
46
70
|
*/
|
|
47
71
|
readonly ident: typeof ident;
|
|
72
|
+
/**
|
|
73
|
+
* 文字列を安全にエスケープするための関数です。
|
|
74
|
+
*/
|
|
75
|
+
readonly literal: typeof literal;
|
|
48
76
|
};
|
|
49
77
|
export { sql };
|
|
50
78
|
//# sourceMappingURL=sql.d.ts.map
|
package/dist/src/sql.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/sql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/sql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAExE,kBAAU,GAAG,CAAC;IACZ;;OAEG;IACH,KAAY,QAAQ,GAAG,OAAO,WAAW,EAAE,QAAQ,CAAC;IAEpD;;OAEG;IACH,KAAY,KAAK,GAAG,OAAO,WAAW,EAAE,KAAK,CAAC;IAE9C;;;;OAIG;IACH,KAAY,IAAI,GAAG,OAAO,WAAW,EAAE,IAAI,CAAC;IAE5C;;OAEG;IACH,KAAY,GAAG,GAAG,OAAO,WAAW,EAAE,GAAG,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,iBAAS,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC;AAMhE;;;;;;;;;;GAUG;AACH,QAAA,MAAM,GAAG,aACe,oBAAoB,eAAe,SAAS,GAAG,CAAC,QAAQ,EAAE,KAAG,GAAG,CAAC,GAAG;IAIxF;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;;;OAIG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;CAGN,CAAC;AAEF,OAAO,EAAE,GAAG,EAAE,CAAC"}
|
package/dist/src/sql.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { empty, join, raw, Sql, ident } from "./core.js";
|
|
1
|
+
import { empty, join, raw, Sql, ident, literal, Slot } from "./core.js";
|
|
2
|
+
function slot(...args) {
|
|
3
|
+
return new sql.Slot(...args);
|
|
4
|
+
}
|
|
2
5
|
/**
|
|
3
6
|
* テンプレートリテラルを使用して SQL クエリーを安全に構築するためのタグ関数です。
|
|
4
7
|
*
|
|
@@ -14,13 +17,23 @@ const sql = /*#__PURE__*/ Object.assign(function sql(strings, ...bindings) {
|
|
|
14
17
|
return new Sql(strings, bindings);
|
|
15
18
|
}, {
|
|
16
19
|
/**
|
|
17
|
-
* Sql
|
|
20
|
+
* Sql クラスです。
|
|
18
21
|
*/
|
|
19
22
|
Sql,
|
|
20
23
|
/**
|
|
21
24
|
* 生の文字列を SQL 断片として扱うための関数です。
|
|
22
25
|
*/
|
|
23
26
|
raw,
|
|
27
|
+
/**
|
|
28
|
+
* 新しい Slot インスタンスを作成する関数です。
|
|
29
|
+
*/
|
|
30
|
+
slot,
|
|
31
|
+
/**
|
|
32
|
+
* スロットを表すクラスです。
|
|
33
|
+
*
|
|
34
|
+
* スロットは後から値を注入可能なプレースホルダーです。
|
|
35
|
+
*/
|
|
36
|
+
Slot,
|
|
24
37
|
/**
|
|
25
38
|
* 複数の SQL 断片を結合するための関数です。
|
|
26
39
|
*/
|
|
@@ -33,5 +46,9 @@ const sql = /*#__PURE__*/ Object.assign(function sql(strings, ...bindings) {
|
|
|
33
46
|
* 識別子(テーブル名等)を安全にエスケープするための関数です。
|
|
34
47
|
*/
|
|
35
48
|
ident,
|
|
49
|
+
/**
|
|
50
|
+
* 文字列を安全にエスケープするための関数です。
|
|
51
|
+
*/
|
|
52
|
+
literal,
|
|
36
53
|
});
|
|
37
54
|
export { sql };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgsql-template-tag",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "",
|
|
5
5
|
"homepage": "https://github.com/tai-kun/pgsql-template-tag",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"default": "./dist/src/index.js"
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"es-toolkit": "^1.46.1"
|
|
30
|
+
},
|
|
28
31
|
"devDependencies": {
|
|
29
32
|
"@tsconfig/node24": "^24.0.4",
|
|
30
33
|
"@tsconfig/strictest": "^2.0.8",
|
|
@@ -33,9 +36,9 @@
|
|
|
33
36
|
"@vitest/browser": "^4.1.5",
|
|
34
37
|
"@vitest/browser-playwright": "^4.1.5",
|
|
35
38
|
"npm-check-updates": "^20.0.2",
|
|
36
|
-
"oxfmt": "^0.
|
|
37
|
-
"oxlint": "^1.
|
|
38
|
-
"oxlint-tsgolint": "^0.22.
|
|
39
|
+
"oxfmt": "^0.47.0",
|
|
40
|
+
"oxlint": "^1.62.0",
|
|
41
|
+
"oxlint-tsgolint": "^0.22.1",
|
|
39
42
|
"playwright": "^1.59.1",
|
|
40
43
|
"typescript": "^6.0.3",
|
|
41
44
|
"vite": "^8.0.10",
|
package/src/core.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isPlainObject } from "es-toolkit/predicate";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* SQL クエリー内で使用される値の型定義です。
|
|
3
5
|
*/
|
|
@@ -7,7 +9,39 @@ export type Value = unknown;
|
|
|
7
9
|
* SQL クエリーの構築に使用できる生の値、または別の Sql インスタンスを表す型です。
|
|
8
10
|
*/
|
|
9
11
|
// oxlint-disable-next-line typescript/no-redundant-type-constituents
|
|
10
|
-
export type RawValue = Value | Sql;
|
|
12
|
+
export type RawValue = Value | Slot | Sql;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* スロットを表すクラスです。
|
|
16
|
+
*
|
|
17
|
+
* スロットは後から値を注入可能なプレースホルダーです。
|
|
18
|
+
*/
|
|
19
|
+
export class Slot {
|
|
20
|
+
/**
|
|
21
|
+
* スロット名です。
|
|
22
|
+
*/
|
|
23
|
+
public readonly name: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* デフォルト値です。
|
|
27
|
+
*/
|
|
28
|
+
public readonly defaultValue: Value;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 新しい Slot インスタンスを初期化します。
|
|
32
|
+
*
|
|
33
|
+
* @param name スロット名です。
|
|
34
|
+
* @param defaultValue デフォルト値です。
|
|
35
|
+
*/
|
|
36
|
+
public constructor(name: string, defaultValue?: Value) {
|
|
37
|
+
if (arguments.length < 2) {
|
|
38
|
+
defaultValue = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.name = name;
|
|
42
|
+
this.defaultValue = defaultValue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
11
45
|
|
|
12
46
|
/**
|
|
13
47
|
* Sql クラスの内部状態を管理するためのプライベートな型定義です。
|
|
@@ -16,21 +50,32 @@ type PrivateState = {
|
|
|
16
50
|
/**
|
|
17
51
|
* クエリーを構成する静的な文字列の配列です。
|
|
18
52
|
*/
|
|
19
|
-
readonly
|
|
53
|
+
readonly parts: readonly [string, ...string[]];
|
|
20
54
|
|
|
21
55
|
/**
|
|
22
56
|
* プレースホルダーに対応するインデックス(1 から始まる数値)の配列です。
|
|
23
57
|
*/
|
|
24
|
-
readonly
|
|
58
|
+
readonly phIds: readonly number[];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* values のインデックス -> スロット情報 のマップです。
|
|
62
|
+
*/
|
|
63
|
+
readonly idx2slot: ReadonlyMap<number, Slot>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* スロット名 -> values のインデックス のマップです。
|
|
67
|
+
*/
|
|
68
|
+
readonly slot2idx: ReadonlyMap<string, ReadonlySet<number>>;
|
|
25
69
|
|
|
26
70
|
/**
|
|
27
71
|
* キャッシュされた最終的なクエリーテキストです。
|
|
28
72
|
*/
|
|
29
|
-
|
|
73
|
+
text?: string;
|
|
30
74
|
};
|
|
31
75
|
|
|
32
76
|
/**
|
|
33
77
|
* 安全な SQL クエリーを構築するためのクラスです。
|
|
78
|
+
*
|
|
34
79
|
* プレースホルダーを使用したパラメーター化クエリーを生成します。
|
|
35
80
|
*/
|
|
36
81
|
export class Sql {
|
|
@@ -43,19 +88,19 @@ export class Sql {
|
|
|
43
88
|
*/
|
|
44
89
|
public get text(): string {
|
|
45
90
|
// キャッシュが存在しない場合にのみ、文字列を構築します。
|
|
46
|
-
if (this._.
|
|
91
|
+
if (this._.text === undefined) {
|
|
47
92
|
let i = 0,
|
|
48
|
-
text = this._.
|
|
93
|
+
text = this._.parts[0];
|
|
49
94
|
|
|
50
95
|
// 文字列の断片とプレースホルダー($1, $2...)を交互に結合します。
|
|
51
|
-
for (; i < this._.
|
|
52
|
-
text += "$" + this._.
|
|
96
|
+
for (; i < this._.phIds.length; i++) {
|
|
97
|
+
text += "$" + this._.phIds[i] + this._.parts[i + 1];
|
|
53
98
|
}
|
|
54
99
|
|
|
55
|
-
this._.
|
|
100
|
+
this._.text = text;
|
|
56
101
|
}
|
|
57
102
|
|
|
58
|
-
return this._.
|
|
103
|
+
return this._.text;
|
|
59
104
|
}
|
|
60
105
|
|
|
61
106
|
/**
|
|
@@ -88,10 +133,52 @@ export class Sql {
|
|
|
88
133
|
const strings: [string, ...string[]] = [rawStrings[0]!];
|
|
89
134
|
const bindings: Value[] = [];
|
|
90
135
|
const placeholderIds: number[] = [];
|
|
136
|
+
const idx2slot = new Map<number, Slot>();
|
|
137
|
+
const slot2idx = new Map<string, Set<number>>();
|
|
91
138
|
|
|
92
139
|
/** 値の重複を排除し、同じ値には同じプレースホルダー ID を割り当てるためのマップです。 */
|
|
93
140
|
const valueToId = new Map<Value, number>();
|
|
94
141
|
|
|
142
|
+
/** スロットごとのプレースホルダー ID を管理します。 */
|
|
143
|
+
const slotToId = new Map<Slot, number>();
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 値を bindings に登録し、placeholderId を取得します。
|
|
147
|
+
*/
|
|
148
|
+
const registerValue = (value: Value): number => {
|
|
149
|
+
let placeholderId = valueToId.get(value);
|
|
150
|
+
if (placeholderId === undefined) {
|
|
151
|
+
bindings.push(value);
|
|
152
|
+
placeholderId = bindings.length;
|
|
153
|
+
valueToId.set(value, placeholderId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return placeholderId;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* スロットを bindings に登録し、placeholderId を取得します。
|
|
161
|
+
*/
|
|
162
|
+
const registerSlot = (slot: Slot): number => {
|
|
163
|
+
let placeholderId = slotToId.get(slot);
|
|
164
|
+
if (placeholderId === undefined) {
|
|
165
|
+
bindings.push(slot.defaultValue);
|
|
166
|
+
placeholderId = bindings.length;
|
|
167
|
+
slotToId.set(slot, placeholderId);
|
|
168
|
+
|
|
169
|
+
const index = placeholderId - 1;
|
|
170
|
+
idx2slot.set(index, slot);
|
|
171
|
+
let idxes = slot2idx.get(slot.name);
|
|
172
|
+
if (idxes === undefined) {
|
|
173
|
+
idxes = new Set();
|
|
174
|
+
slot2idx.set(slot.name, idxes);
|
|
175
|
+
}
|
|
176
|
+
idxes.add(index);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return placeholderId;
|
|
180
|
+
};
|
|
181
|
+
|
|
95
182
|
// 提供された全てのバインディング値を走査して、SQL 文字列と値を正規化します。
|
|
96
183
|
for (let i = 0; i < rawBindings.length; i++) {
|
|
97
184
|
const child = rawBindings[i];
|
|
@@ -100,33 +187,26 @@ export class Sql {
|
|
|
100
187
|
// バインディング値が Sql インスタンス(ネストされたクエリー)の場合の処理です。
|
|
101
188
|
if (child instanceof Sql) {
|
|
102
189
|
// 現在の最後の文字列断片に、ネストされた Sql の最初の断片を結合します。
|
|
103
|
-
strings[strings.length - 1] += child._.
|
|
190
|
+
strings[strings.length - 1] += child._.parts[0];
|
|
104
191
|
|
|
105
192
|
// ネストされた Sql のプレースホルダーと値を再マッピングします。
|
|
106
|
-
for (let j = 0; j < child._.
|
|
107
|
-
const childPlaceholderId = child._.
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
valueToId.set(value, placeholderId);
|
|
115
|
-
}
|
|
193
|
+
for (let j = 0; j < child._.phIds.length; j++) {
|
|
194
|
+
const childPlaceholderId = child._.phIds[j]!;
|
|
195
|
+
const valueIndex = childPlaceholderId - 1;
|
|
196
|
+
const value = child.values[valueIndex]!;
|
|
197
|
+
|
|
198
|
+
const slot = child._.idx2slot.get(valueIndex);
|
|
199
|
+
|
|
200
|
+
const placeholderId = slot !== undefined ? registerSlot(slot) : registerValue(value);
|
|
116
201
|
|
|
117
|
-
strings.push(child._.
|
|
202
|
+
strings.push(child._.parts[j + 1]!);
|
|
118
203
|
placeholderIds.push(placeholderId);
|
|
119
204
|
}
|
|
120
205
|
|
|
121
206
|
// ネストされた Sql の展開が終わった後に、後続の生の文字列を結合します。
|
|
122
207
|
strings[strings.length - 1] += rawString;
|
|
123
208
|
} else {
|
|
124
|
-
|
|
125
|
-
if (placeholderId === undefined) {
|
|
126
|
-
bindings.push(child);
|
|
127
|
-
placeholderId = bindings.length;
|
|
128
|
-
valueToId.set(child, placeholderId);
|
|
129
|
-
}
|
|
209
|
+
const placeholderId = child instanceof Slot ? registerSlot(child) : registerValue(child);
|
|
130
210
|
|
|
131
211
|
strings.push(rawString);
|
|
132
212
|
placeholderIds.push(placeholderId);
|
|
@@ -138,12 +218,57 @@ export class Sql {
|
|
|
138
218
|
// 内部状態を隠蔽し、不必要なプロパティーの露出を防ぎます。
|
|
139
219
|
Object.defineProperty(this, "_", {
|
|
140
220
|
value: {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
221
|
+
parts: strings,
|
|
222
|
+
phIds: placeholderIds,
|
|
223
|
+
idx2slot,
|
|
224
|
+
slot2idx,
|
|
225
|
+
} satisfies PrivateState,
|
|
144
226
|
});
|
|
145
227
|
}
|
|
146
228
|
|
|
229
|
+
/**
|
|
230
|
+
* スロットを値で埋めます。
|
|
231
|
+
*
|
|
232
|
+
* @param slots スロットの値です。
|
|
233
|
+
* @returns スロットが埋められた新しい Sql インスタンスです。
|
|
234
|
+
*/
|
|
235
|
+
public fill(
|
|
236
|
+
slots: { readonly [name: string]: Value } | Iterable<readonly [string | Slot, Value]>,
|
|
237
|
+
): Sql {
|
|
238
|
+
if (isPlainObject(slots)) {
|
|
239
|
+
slots = Object.entries(slots);
|
|
240
|
+
} else {
|
|
241
|
+
// 一旦 Map のインスタンスにすることで、重複する名前またはスロットを 1 つに絞ります。
|
|
242
|
+
slots = new Map(slots);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const values: Value[] = [...this.values];
|
|
246
|
+
for (const [target, value] of new Map(slots)) {
|
|
247
|
+
let idxes: ReadonlySet<number> | undefined;
|
|
248
|
+
if (typeof target === "string") {
|
|
249
|
+
idxes = this._.slot2idx.get(target);
|
|
250
|
+
} else {
|
|
251
|
+
for (const [idx, slot] of this._.idx2slot) {
|
|
252
|
+
if (slot === target) {
|
|
253
|
+
idxes ||= new Set();
|
|
254
|
+
(idxes as Set<number>).add(idx);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (idxes === undefined) {
|
|
260
|
+
// スロットが見つからない場合は無視します。
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const idx of idxes) {
|
|
265
|
+
values[idx] = value;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return new Sql(this._.parts, values);
|
|
270
|
+
}
|
|
271
|
+
|
|
147
272
|
/**
|
|
148
273
|
* オブジェクトを JSON 形式に変換可能な形式で返します。
|
|
149
274
|
*
|
|
@@ -171,6 +296,7 @@ export class Sql {
|
|
|
171
296
|
|
|
172
297
|
/**
|
|
173
298
|
* 生の文字列を SQL 断片として扱います。
|
|
299
|
+
*
|
|
174
300
|
* この値はパラメーター化の対象にならず、そのままクエリーに含まれます。
|
|
175
301
|
*
|
|
176
302
|
* @param value SQL に含める生の文字列です。
|
|
@@ -218,3 +344,20 @@ const DOUBLE_QUOTE_REGEX = /"/g;
|
|
|
218
344
|
export function ident(value: string): string {
|
|
219
345
|
return '"' + value.replace(DOUBLE_QUOTE_REGEX, '""') + '"';
|
|
220
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* 一重引用符をエスケープするための正規表現です。
|
|
350
|
+
*/
|
|
351
|
+
const SINGLE_QUOTE_REGEX = /'/g;
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 文字列を SQL のリテラル文字列として安全にエスケープします。
|
|
355
|
+
*
|
|
356
|
+
* 一重引用符を二重にすることでエスケープを行い、全体を一重引用符で囲みます。
|
|
357
|
+
*
|
|
358
|
+
* @param value エスケープする文字列です。
|
|
359
|
+
* @returns エスケープ済みのリテラル文字列を返します。
|
|
360
|
+
*/
|
|
361
|
+
export function literal(value: string): string {
|
|
362
|
+
return "'" + value.replace(SINGLE_QUOTE_REGEX, "''") + "'";
|
|
363
|
+
}
|
package/src/index.ts
CHANGED
package/src/sql.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { empty, join, raw, Sql, ident } from "./core.js";
|
|
1
|
+
import { empty, join, raw, Sql, ident, literal, Slot } from "./core.js";
|
|
2
2
|
|
|
3
3
|
namespace sql {
|
|
4
4
|
/**
|
|
@@ -11,12 +11,32 @@ namespace sql {
|
|
|
11
11
|
*/
|
|
12
12
|
export type Value = import("./core.js").Value;
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* スロットを表すクラスです。
|
|
16
|
+
*
|
|
17
|
+
* スロットは後から値を注入可能なプレースホルダーです。
|
|
18
|
+
*/
|
|
19
|
+
export type Slot = import("./core.js").Slot;
|
|
20
|
+
|
|
14
21
|
/**
|
|
15
22
|
* 安全な SQL クエリーを構築するためのクラス型です。
|
|
16
23
|
*/
|
|
17
24
|
export type Sql = import("./core.js").Sql;
|
|
18
25
|
}
|
|
19
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 新しい Slot インスタンスを作成します。
|
|
29
|
+
*
|
|
30
|
+
* @param name スロット名です。
|
|
31
|
+
* @param defaultValue デフォルト値です。
|
|
32
|
+
* @returns 作成された新しい Slot インスタンスです。
|
|
33
|
+
*/
|
|
34
|
+
function slot(name: string, defaultValue?: sql.Value): sql.Slot;
|
|
35
|
+
|
|
36
|
+
function slot(...args: [any]): sql.Slot {
|
|
37
|
+
return new sql.Slot(...args);
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
/**
|
|
21
41
|
* テンプレートリテラルを使用して SQL クエリーを安全に構築するためのタグ関数です。
|
|
22
42
|
*
|
|
@@ -34,7 +54,7 @@ const sql = /*#__PURE__*/ Object.assign(
|
|
|
34
54
|
},
|
|
35
55
|
{
|
|
36
56
|
/**
|
|
37
|
-
* Sql
|
|
57
|
+
* Sql クラスです。
|
|
38
58
|
*/
|
|
39
59
|
Sql,
|
|
40
60
|
|
|
@@ -43,6 +63,18 @@ const sql = /*#__PURE__*/ Object.assign(
|
|
|
43
63
|
*/
|
|
44
64
|
raw,
|
|
45
65
|
|
|
66
|
+
/**
|
|
67
|
+
* 新しい Slot インスタンスを作成する関数です。
|
|
68
|
+
*/
|
|
69
|
+
slot,
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* スロットを表すクラスです。
|
|
73
|
+
*
|
|
74
|
+
* スロットは後から値を注入可能なプレースホルダーです。
|
|
75
|
+
*/
|
|
76
|
+
Slot,
|
|
77
|
+
|
|
46
78
|
/**
|
|
47
79
|
* 複数の SQL 断片を結合するための関数です。
|
|
48
80
|
*/
|
|
@@ -57,6 +89,11 @@ const sql = /*#__PURE__*/ Object.assign(
|
|
|
57
89
|
* 識別子(テーブル名等)を安全にエスケープするための関数です。
|
|
58
90
|
*/
|
|
59
91
|
ident,
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 文字列を安全にエスケープするための関数です。
|
|
95
|
+
*/
|
|
96
|
+
literal,
|
|
60
97
|
} as const,
|
|
61
98
|
);
|
|
62
99
|
|