@kubun/db-adapter 0.12.0 → 0.13.1
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/lib/postgres.d.ts +5 -1
- package/lib/postgres.js +34 -0
- package/lib/sqlite.d.ts +5 -1
- package/lib/sqlite.js +49 -9
- package/lib/types.d.ts +5 -0
- package/package.json +5 -2
package/lib/postgres.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Expression, Kysely, RawBuilder } from 'kysely';
|
|
2
|
-
import type { AbstractAdapter, AdapterTypes, ColumnTypes, Functions, NullOrderingTerm, OrderDirection, SearchHit, SearchIndexConfig, SearchQuery } from './types.js';
|
|
2
|
+
import type { AbstractAdapter, AdapterTypes, ArrayPresenceMode, ColumnTypes, Functions, NullOrderingTerm, OrderDirection, SearchHit, SearchIndexConfig, SearchQuery } from './types.js';
|
|
3
3
|
export type PostgresTypes = {
|
|
4
4
|
Binary: Uint8Array;
|
|
5
5
|
JSON: unknown;
|
|
@@ -15,6 +15,10 @@ export declare class AbstractPostgresAdapter<T extends AdapterTypes = PostgresTy
|
|
|
15
15
|
coerceFilterValue(value: unknown): unknown;
|
|
16
16
|
numericCast(expression: Expression<unknown>): Expression<number>;
|
|
17
17
|
nullOrdering(_expression: Expression<unknown>, _direction: OrderDirection): NullOrderingTerm;
|
|
18
|
+
containsPredicate(expression: Expression<unknown>, pattern: string): RawBuilder<boolean>;
|
|
19
|
+
arrayIncludesAllPredicate(expression: Expression<unknown>, values: Array<unknown>): RawBuilder<boolean>;
|
|
20
|
+
arrayIncludesAnyPredicate(expression: Expression<unknown>, values: Array<unknown>): RawBuilder<boolean>;
|
|
21
|
+
arrayPresencePredicate(expression: Expression<unknown>, mode: ArrayPresenceMode): RawBuilder<boolean>;
|
|
18
22
|
readAccessPredicate(params: {
|
|
19
23
|
viewerDID: string;
|
|
20
24
|
groupIDs: Array<string>;
|
package/lib/postgres.js
CHANGED
|
@@ -41,6 +41,40 @@ export class AbstractPostgresAdapter {
|
|
|
41
41
|
kind: 'native'
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
containsPredicate(expression, pattern) {
|
|
45
|
+
return sql`${expression} ILIKE ${pattern} ESCAPE '\\'`;
|
|
46
|
+
}
|
|
47
|
+
arrayIncludesAllPredicate(expression, values) {
|
|
48
|
+
// Native jsonb containment: works for string and numeric elements alike. NULL node
|
|
49
|
+
// coalesces to an empty array, which is a subset only of another empty set.
|
|
50
|
+
// Cast via ::text::jsonb, not a direct ::jsonb: postgres.js infers the parameter's
|
|
51
|
+
// wire type from a trailing ::jsonb cast and re-encodes the JS string as a jsonb
|
|
52
|
+
// *string scalar* instead of sending it as text for the server to parse, so a
|
|
53
|
+
// direct ::jsonb cast silently never matches. The ::text hop forces text encoding.
|
|
54
|
+
return sql`COALESCE(${expression}, '[]'::jsonb) @> ${JSON.stringify(values)}::text::jsonb`;
|
|
55
|
+
}
|
|
56
|
+
arrayIncludesAnyPredicate(expression, values) {
|
|
57
|
+
// Elements, not `?|`, so numeric arrays are covered too (`?|` only matches string keys).
|
|
58
|
+
return sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(${expression}, '[]'::jsonb)) AS _t WHERE _t IN (${sql.join(values.map((v)=>sql`${v}`))}))`;
|
|
59
|
+
}
|
|
60
|
+
arrayPresencePredicate(expression, mode) {
|
|
61
|
+
// null/nonNull/empty/nonEmpty must see the true NULL, so the node is never coalesced
|
|
62
|
+
// there — jsonb_array_length(NULL) is NULL, and NULL > 0 is not-true (excludes it).
|
|
63
|
+
switch(mode){
|
|
64
|
+
case 'null':
|
|
65
|
+
return sql`${expression} IS NULL`;
|
|
66
|
+
case 'nonNull':
|
|
67
|
+
return sql`${expression} IS NOT NULL`;
|
|
68
|
+
case 'empty':
|
|
69
|
+
return sql`jsonb_array_length(${expression}) = 0`;
|
|
70
|
+
case 'nonEmpty':
|
|
71
|
+
return sql`jsonb_array_length(${expression}) > 0`;
|
|
72
|
+
case 'nullOrEmpty':
|
|
73
|
+
return sql`COALESCE(jsonb_array_length(${expression}), 0) = 0`;
|
|
74
|
+
default:
|
|
75
|
+
throw new Error(`Invalid array presence mode: ${mode}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
44
78
|
readAccessPredicate(params, modelID) {
|
|
45
79
|
const { viewerDID, groupIDs, circleIDs, serverDefault } = params;
|
|
46
80
|
const docRestrictedParts = [
|
package/lib/sqlite.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Expression, Kysely, RawBuilder } from 'kysely';
|
|
2
|
-
import type { AbstractAdapter, AdapterTypes, ColumnTypes, Functions, NullOrderingTerm, OrderDirection, SearchHit, SearchIndexConfig, SearchQuery } from './types.js';
|
|
2
|
+
import type { AbstractAdapter, AdapterTypes, ArrayPresenceMode, ColumnTypes, Functions, NullOrderingTerm, OrderDirection, SearchHit, SearchIndexConfig, SearchQuery } from './types.js';
|
|
3
3
|
export type SQLiteTypes = {
|
|
4
4
|
Binary: Uint8Array;
|
|
5
5
|
JSON: string;
|
|
@@ -15,6 +15,10 @@ export declare class AbstractSQLiteAdapter<T extends AdapterTypes = SQLiteTypes>
|
|
|
15
15
|
coerceFilterValue(value: unknown): unknown;
|
|
16
16
|
numericCast(expression: Expression<unknown>): Expression<number>;
|
|
17
17
|
nullOrdering(expression: Expression<unknown>, direction: OrderDirection): NullOrderingTerm;
|
|
18
|
+
containsPredicate(expression: Expression<unknown>, pattern: string): RawBuilder<boolean>;
|
|
19
|
+
arrayIncludesAllPredicate(expression: Expression<unknown>, values: Array<unknown>): RawBuilder<boolean>;
|
|
20
|
+
arrayIncludesAnyPredicate(expression: Expression<unknown>, values: Array<unknown>): RawBuilder<boolean>;
|
|
21
|
+
arrayPresencePredicate(expression: Expression<unknown>, mode: ArrayPresenceMode): RawBuilder<boolean>;
|
|
18
22
|
readAccessPredicate(params: {
|
|
19
23
|
viewerDID: string;
|
|
20
24
|
groupIDs: Array<string>;
|
package/lib/sqlite.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { sql } from 'kysely';
|
|
2
|
+
const SEARCH_FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3
|
+
function assertSearchFieldNames(fields) {
|
|
4
|
+
for (const field of fields){
|
|
5
|
+
if (!SEARCH_FIELD_NAME.test(field)) {
|
|
6
|
+
throw new Error(`invalid search field name: ${JSON.stringify(field)}`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
2
10
|
export class AbstractSQLiteAdapter {
|
|
3
11
|
functions = {
|
|
4
12
|
now: sql`(unixepoch())`
|
|
@@ -45,6 +53,36 @@ export class AbstractSQLiteAdapter {
|
|
|
45
53
|
expression: sql`CASE WHEN ${expression} IS NULL THEN ${sql.lit(nullRank)} ELSE ${sql.lit(valueRank)} END`
|
|
46
54
|
};
|
|
47
55
|
}
|
|
56
|
+
containsPredicate(expression, pattern) {
|
|
57
|
+
// SQLite's LIKE is already ASCII case-insensitive by default.
|
|
58
|
+
return sql`${expression} LIKE ${pattern} ESCAPE '\\'`;
|
|
59
|
+
}
|
|
60
|
+
arrayIncludesAllPredicate(expression, values) {
|
|
61
|
+
// NULL/absent node -> json_each yields zero rows -> COUNT(DISTINCT value) = 0, never
|
|
62
|
+
// equal to a positive set size.
|
|
63
|
+
return sql`(SELECT COUNT(DISTINCT value) FROM json_each(COALESCE(${expression}, '[]')) WHERE value IN (${sql.join(values.map((v)=>sql`${v}`))})) = ${values.length}`;
|
|
64
|
+
}
|
|
65
|
+
arrayIncludesAnyPredicate(expression, values) {
|
|
66
|
+
return sql`EXISTS (SELECT 1 FROM json_each(COALESCE(${expression}, '[]')) WHERE value IN (${sql.join(values.map((v)=>sql`${v}`))}))`;
|
|
67
|
+
}
|
|
68
|
+
arrayPresencePredicate(expression, mode) {
|
|
69
|
+
// null/nonNull/empty/nonEmpty must see the true NULL, so the node is never coalesced
|
|
70
|
+
// there — json_array_length(NULL) is NULL, and NULL > 0 is not-true (excludes it).
|
|
71
|
+
switch(mode){
|
|
72
|
+
case 'null':
|
|
73
|
+
return sql`${expression} IS NULL`;
|
|
74
|
+
case 'nonNull':
|
|
75
|
+
return sql`${expression} IS NOT NULL`;
|
|
76
|
+
case 'empty':
|
|
77
|
+
return sql`json_array_length(${expression}) = 0`;
|
|
78
|
+
case 'nonEmpty':
|
|
79
|
+
return sql`json_array_length(${expression}) > 0`;
|
|
80
|
+
case 'nullOrEmpty':
|
|
81
|
+
return sql`COALESCE(json_array_length(${expression}), 0) = 0`;
|
|
82
|
+
default:
|
|
83
|
+
throw new Error(`Invalid array presence mode: ${mode}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
48
86
|
readAccessPredicate(params, modelID) {
|
|
49
87
|
const { viewerDID, groupIDs, circleIDs, serverDefault } = params;
|
|
50
88
|
const docRestrictedParts = [
|
|
@@ -91,29 +129,31 @@ export class AbstractSQLiteAdapter {
|
|
|
91
129
|
)`;
|
|
92
130
|
}
|
|
93
131
|
async createSearchIndex(db, config) {
|
|
132
|
+
assertSearchFieldNames(config.fields);
|
|
94
133
|
const tableName = `fts_${config.modelID}`;
|
|
95
134
|
const columns = [
|
|
96
|
-
'document_id UNINDEXED
|
|
97
|
-
...config.fields
|
|
98
|
-
]
|
|
99
|
-
await sql`CREATE VIRTUAL TABLE IF NOT EXISTS ${sql.ref(tableName)} USING fts5(${sql.
|
|
135
|
+
sql`${sql.ref('document_id')} UNINDEXED`,
|
|
136
|
+
...config.fields.map((f)=>sql.ref(f))
|
|
137
|
+
];
|
|
138
|
+
await sql`CREATE VIRTUAL TABLE IF NOT EXISTS ${sql.ref(tableName)} USING fts5(${sql.join(columns)})`.execute(db);
|
|
100
139
|
}
|
|
101
140
|
async dropSearchIndex(db, modelID) {
|
|
102
141
|
const tableName = `fts_${modelID}`;
|
|
103
142
|
await sql`DROP TABLE IF EXISTS ${sql.ref(tableName)}`.execute(db);
|
|
104
143
|
}
|
|
105
144
|
async updateSearchEntry(db, modelID, documentID, fields, fieldNames) {
|
|
145
|
+
assertSearchFieldNames(fieldNames);
|
|
106
146
|
const tableName = `fts_${modelID}`;
|
|
107
147
|
await sql`DELETE FROM ${sql.ref(tableName)} WHERE document_id = ${documentID}`.execute(db);
|
|
108
148
|
const columns = [
|
|
109
|
-
'document_id',
|
|
110
|
-
...fieldNames
|
|
111
|
-
]
|
|
149
|
+
sql.ref('document_id'),
|
|
150
|
+
...fieldNames.map((f)=>sql.ref(f))
|
|
151
|
+
];
|
|
112
152
|
const values = [
|
|
113
153
|
documentID,
|
|
114
154
|
...fieldNames.map((f)=>fields[f] ?? '')
|
|
115
|
-
]
|
|
116
|
-
await sql`INSERT INTO ${sql.ref(tableName)} (${sql.
|
|
155
|
+
];
|
|
156
|
+
await sql`INSERT INTO ${sql.ref(tableName)} (${sql.join(columns)}) VALUES (${sql.join(values.map((v)=>sql`${v}`))})`.execute(db);
|
|
117
157
|
}
|
|
118
158
|
async removeSearchEntry(db, modelID, documentID) {
|
|
119
159
|
const tableName = `fts_${modelID}`;
|
package/lib/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ColumnDataType, ColumnType, Dialect, Expression, Kysely, RawBuilder } from 'kysely';
|
|
2
2
|
export type OrderDirection = 'asc' | 'desc';
|
|
3
|
+
export type ArrayPresenceMode = 'null' | 'nonNull' | 'empty' | 'nonEmpty' | 'nullOrEmpty';
|
|
3
4
|
export type NullOrderingTerm = {
|
|
4
5
|
kind: 'lead';
|
|
5
6
|
expression: Expression<number>;
|
|
@@ -53,6 +54,10 @@ export type AbstractAdapter<T extends AdapterTypes = AdapterTypes> = {
|
|
|
53
54
|
coerceFilterValue(value: unknown): unknown;
|
|
54
55
|
numericCast(expression: Expression<unknown>): Expression<number>;
|
|
55
56
|
nullOrdering(expression: Expression<unknown>, direction: OrderDirection): NullOrderingTerm;
|
|
57
|
+
containsPredicate(expression: Expression<unknown>, pattern: string): RawBuilder<boolean>;
|
|
58
|
+
arrayIncludesAllPredicate(expression: Expression<unknown>, values: Array<unknown>): RawBuilder<boolean>;
|
|
59
|
+
arrayIncludesAnyPredicate(expression: Expression<unknown>, values: Array<unknown>): RawBuilder<boolean>;
|
|
60
|
+
arrayPresencePredicate(expression: Expression<unknown>, mode: ArrayPresenceMode): RawBuilder<boolean>;
|
|
56
61
|
readAccessPredicate(params: {
|
|
57
62
|
viewerDID: string;
|
|
58
63
|
groupIDs: Array<string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/db-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
"LICENSE.md"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"kysely": "^0.29.
|
|
18
|
+
"kysely": "^0.29.5"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
19
22
|
},
|
|
20
23
|
"scripts": {
|
|
21
24
|
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|