@kubun/db-adapter 0.10.0 → 0.12.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.
package/lib/index.js CHANGED
@@ -1 +1,2 @@
1
- export{AbstractPostgresAdapter}from"./postgres.js";export{AbstractSQLiteAdapter}from"./sqlite.js";
1
+ export { AbstractPostgresAdapter } from './postgres.js';
2
+ export { AbstractSQLiteAdapter } from './sqlite.js';
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, SearchHit, SearchIndexConfig, SearchQuery } from './types.js';
2
+ import type { AbstractAdapter, AdapterTypes, ColumnTypes, Functions, NullOrderingTerm, OrderDirection, SearchHit, SearchIndexConfig, SearchQuery } from './types.js';
3
3
  export type PostgresTypes = {
4
4
  Binary: Uint8Array;
5
5
  JSON: unknown;
@@ -14,6 +14,7 @@ export declare class AbstractPostgresAdapter<T extends AdapterTypes = PostgresTy
14
14
  decodeTimestamp(value: string): Date;
15
15
  coerceFilterValue(value: unknown): unknown;
16
16
  numericCast(expression: Expression<unknown>): Expression<number>;
17
+ nullOrdering(_expression: Expression<unknown>, _direction: OrderDirection): NullOrderingTerm;
17
18
  readAccessPredicate(params: {
18
19
  viewerDID: string;
19
20
  groupIDs: Array<string>;
package/lib/postgres.js CHANGED
@@ -1,37 +1,138 @@
1
- import{sql as e}from"kysely";export class AbstractPostgresAdapter{functions={now:"now()"};types={binary:"bytea",json:"jsonb",serial:"serial",text:"text",timestamp:"timestamptz(3)",uuid:"uuid"};encodeBinary(e){return e}encodeJSON(e){return e}encodeTimestamp(e){return e.toISOString()}decodeTimestamp(e){return new Date(e)}coerceFilterValue(e){return"boolean"==typeof e?e?"true":"false":e}numericCast(t){return e`(${t})::numeric`}readAccessPredicate(t,a){let{viewerDID:r,groupIDs:s,circleIDs:n,serverDefault:o}=t,_=[e`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(data -> 'accessPermissions' -> 'read' -> 'allowedDIDs', '[]'::jsonb)) AS _t WHERE _t = ${r})`];s.length>0&&_.push(e`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(data -> 'accessPermissions' -> 'read' -> 'allowedGroups', '[]'::jsonb)) AS _t WHERE _t IN (${e.join(s.map(t=>e`${t}`))}))`),n.length>0&&_.push(e`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(data -> 'accessPermissions' -> 'read' -> 'allowedCircles', '[]'::jsonb)) AS _t WHERE _t IN (${e.join(n.map(t=>e`${t}`))}))`);let d=_.reduce((t,a)=>e`${t} OR ${a}`),E=[e`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(_mad.allowed_dids, '[]'::jsonb)) AS _t WHERE _t = ${r})`];s.length>0&&E.push(e`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(_mad.allowed_groups, '[]'::jsonb)) AS _t WHERE _t IN (${e.join(s.map(t=>e`${t}`))}))`),n.length>0&&E.push(e`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(_mad.allowed_circles, '[]'::jsonb)) AS _t WHERE _t IN (${e.join(n.map(t=>e`${t}`))}))`);let c=E.reduce((t,a)=>e`${t} OR ${a}`),l="anyone"===o?e`true`:"only_owner"===o?e`owner = ${r}`:e`false`;return e`(
2
- owner = ${r}
1
+ import { sql } from 'kysely';
2
+ export class AbstractPostgresAdapter {
3
+ functions = {
4
+ now: 'now()'
5
+ };
6
+ types = {
7
+ bigint: 'bigint',
8
+ binary: 'bytea',
9
+ json: 'jsonb',
10
+ serial: 'serial',
11
+ text: 'text',
12
+ // Use 3 digits for timestamps to have millisecond precision, same as JS dates
13
+ timestamp: 'timestamptz(3)',
14
+ uuid: 'uuid'
15
+ };
16
+ encodeBinary(value) {
17
+ return value;
18
+ }
19
+ encodeJSON(value) {
20
+ return value;
21
+ }
22
+ encodeTimestamp(value) {
23
+ return value.toISOString();
24
+ }
25
+ decodeTimestamp(value) {
26
+ return new Date(value);
27
+ }
28
+ coerceFilterValue(value) {
29
+ if (typeof value === 'boolean') {
30
+ return value ? 'true' : 'false';
31
+ }
32
+ return value;
33
+ }
34
+ numericCast(expression) {
35
+ return sql`(${expression})::numeric`;
36
+ }
37
+ nullOrdering(_expression, _direction) {
38
+ // Postgres applies SQL-standard null position natively (NULLS LAST on asc, NULLS
39
+ // FIRST on desc) via the dialect's own clause on the field's order-by item.
40
+ return {
41
+ kind: 'native'
42
+ };
43
+ }
44
+ readAccessPredicate(params, modelID) {
45
+ const { viewerDID, groupIDs, circleIDs, serverDefault } = params;
46
+ const docRestrictedParts = [
47
+ sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(data -> 'accessPermissions' -> 'read' -> 'allowedDIDs', '[]'::jsonb)) AS _t WHERE _t = ${viewerDID})`
48
+ ];
49
+ if (groupIDs.length > 0) {
50
+ docRestrictedParts.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(data -> 'accessPermissions' -> 'read' -> 'allowedGroups', '[]'::jsonb)) AS _t WHERE _t IN (${sql.join(groupIDs.map((id)=>sql`${id}`))}))`);
51
+ }
52
+ if (circleIDs.length > 0) {
53
+ docRestrictedParts.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(data -> 'accessPermissions' -> 'read' -> 'allowedCircles', '[]'::jsonb)) AS _t WHERE _t IN (${sql.join(circleIDs.map((id)=>sql`${id}`))}))`);
54
+ }
55
+ const docRestricted = docRestrictedParts.reduce((a, b)=>sql`${a} OR ${b}`);
56
+ const madRestrictedParts = [
57
+ sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(_mad.allowed_dids, '[]'::jsonb)) AS _t WHERE _t = ${viewerDID})`
58
+ ];
59
+ if (groupIDs.length > 0) {
60
+ madRestrictedParts.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(_mad.allowed_groups, '[]'::jsonb)) AS _t WHERE _t IN (${sql.join(groupIDs.map((id)=>sql`${id}`))}))`);
61
+ }
62
+ if (circleIDs.length > 0) {
63
+ madRestrictedParts.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(_mad.allowed_circles, '[]'::jsonb)) AS _t WHERE _t IN (${sql.join(circleIDs.map((id)=>sql`${id}`))}))`);
64
+ }
65
+ const madRestricted = madRestrictedParts.reduce((a, b)=>sql`${a} OR ${b}`);
66
+ const serverDefaultCond = serverDefault === 'anyone' ? sql`true` : serverDefault === 'only_owner' ? sql`owner = ${viewerDID}` : sql`false`;
67
+ return sql`(
68
+ owner = ${viewerDID}
3
69
  OR (data -> 'accessPermissions' -> 'read' ->> 'level') = 'anyone'
4
- OR ((data -> 'accessPermissions' -> 'read' ->> 'level') = 'only_owner' AND owner = ${r})
5
- OR ((data -> 'accessPermissions' -> 'read' ->> 'level') = 'restricted' AND (${d}))
70
+ OR ((data -> 'accessPermissions' -> 'read' ->> 'level') = 'only_owner' AND owner = ${viewerDID})
71
+ OR ((data -> 'accessPermissions' -> 'read' ->> 'level') = 'restricted' AND (${docRestricted}))
6
72
  OR ((data -> 'accessPermissions' -> 'read' ->> 'level') IS NULL AND EXISTS (
7
73
  SELECT 1 FROM kubun_graph_user_model_access_defaults AS _mad
8
- WHERE _mad.owner_did = owner AND _mad.model_id = ${a} AND _mad.permission_type = 'read'
74
+ WHERE _mad.owner_did = owner AND _mad.model_id = ${modelID} AND _mad.permission_type = 'read'
75
+ AND (_mad.removed IS NULL OR _mad.removed = 0)
9
76
  AND (
10
77
  _mad.access_level = 'anyone'
11
- OR (_mad.access_level = 'only_owner' AND owner = ${r})
12
- OR (_mad.access_level = 'restricted' AND (${c}))
78
+ OR (_mad.access_level = 'only_owner' AND owner = ${viewerDID})
79
+ OR (_mad.access_level = 'restricted' AND (${madRestricted}))
13
80
  )
14
81
  ))
15
82
  OR ((data -> 'accessPermissions' -> 'read' ->> 'level') IS NULL AND NOT EXISTS (
16
83
  SELECT 1 FROM kubun_graph_user_model_access_defaults AS _mad2
17
- WHERE _mad2.owner_did = owner AND _mad2.model_id = ${a} AND _mad2.permission_type = 'read'
18
- ) AND (${l}))
19
- )`}async createSearchIndex(t,a){let r=`fts_${a.modelID}`,s=`k_${a.modelID}`;await e`
20
- CREATE TABLE IF NOT EXISTS ${e.ref(r)} (
21
- document_id TEXT PRIMARY KEY REFERENCES ${e.ref(s)}(id) ON DELETE CASCADE,
84
+ WHERE _mad2.owner_did = owner AND _mad2.model_id = ${modelID} AND _mad2.permission_type = 'read'
85
+ AND (_mad2.removed IS NULL OR _mad2.removed = 0)
86
+ ) AND (${serverDefaultCond}))
87
+ )`;
88
+ }
89
+ async createSearchIndex(db, config) {
90
+ const tableName = `fts_${config.modelID}`;
91
+ const docTableName = `k_${config.modelID}`;
92
+ await sql`
93
+ CREATE TABLE IF NOT EXISTS ${sql.ref(tableName)} (
94
+ document_id TEXT PRIMARY KEY REFERENCES ${sql.ref(docTableName)}(id) ON DELETE CASCADE,
22
95
  search_vector tsvector
23
96
  )
24
- `.execute(t),await e`
25
- CREATE INDEX IF NOT EXISTS ${e.ref(`ftsi_${a.modelID}`)}
26
- ON ${e.ref(r)} USING GIN(search_vector)
27
- `.execute(t)}async dropSearchIndex(t,a){let r=`fts_${a}`;await e`DROP TABLE IF EXISTS ${e.ref(r)} CASCADE`.execute(t)}async updateSearchEntry(t,a,r,s,n){let o=`fts_${a}`,_=n.map(t=>{let a=s[t]??"";return e`to_tsvector('simple', ${a})`}).reduce((t,a)=>e`${t} || ${a}`);await e`
28
- INSERT INTO ${e.ref(o)} (document_id, search_vector)
29
- VALUES (${r}, ${_})
30
- ON CONFLICT (document_id) DO UPDATE SET search_vector = ${_}
31
- `.execute(t)}async removeSearchEntry(t,a,r){let s=`fts_${a}`;await e`DELETE FROM ${e.ref(s)} WHERE document_id = ${r}`.execute(t)}async searchIndex(t,a){let r=`fts_${a.modelID}`;return(await e`
32
- SELECT document_id, ts_rank(search_vector, plainto_tsquery('simple', ${a.query})) AS rank
33
- FROM ${e.ref(r)}
34
- WHERE search_vector @@ plainto_tsquery('simple', ${a.query})
97
+ `.execute(db);
98
+ await sql`
99
+ CREATE INDEX IF NOT EXISTS ${sql.ref(`ftsi_${config.modelID}`)}
100
+ ON ${sql.ref(tableName)} USING GIN(search_vector)
101
+ `.execute(db);
102
+ }
103
+ async dropSearchIndex(db, modelID) {
104
+ const tableName = `fts_${modelID}`;
105
+ await sql`DROP TABLE IF EXISTS ${sql.ref(tableName)} CASCADE`.execute(db);
106
+ }
107
+ async updateSearchEntry(db, modelID, documentID, fields, fieldNames) {
108
+ const tableName = `fts_${modelID}`;
109
+ const tsvectorParts = fieldNames.map((f)=>{
110
+ const value = fields[f] ?? '';
111
+ return sql`to_tsvector('simple', ${value})`;
112
+ });
113
+ const tsvectorExpr = tsvectorParts.reduce((acc, part)=>sql`${acc} || ${part}`);
114
+ await sql`
115
+ INSERT INTO ${sql.ref(tableName)} (document_id, search_vector)
116
+ VALUES (${documentID}, ${tsvectorExpr})
117
+ ON CONFLICT (document_id) DO UPDATE SET search_vector = ${tsvectorExpr}
118
+ `.execute(db);
119
+ }
120
+ async removeSearchEntry(db, modelID, documentID) {
121
+ const tableName = `fts_${modelID}`;
122
+ await sql`DELETE FROM ${sql.ref(tableName)} WHERE document_id = ${documentID}`.execute(db);
123
+ }
124
+ async searchIndex(db, params) {
125
+ const tableName = `fts_${params.modelID}`;
126
+ const results = await sql`
127
+ SELECT document_id, ts_rank(search_vector, plainto_tsquery('simple', ${params.query})) AS rank
128
+ FROM ${sql.ref(tableName)}
129
+ WHERE search_vector @@ plainto_tsquery('simple', ${params.query})
35
130
  ORDER BY rank DESC
36
- LIMIT ${a.limit}
37
- `.execute(t)).rows.map(e=>({documentID:e.document_id,rank:e.rank}))}}
131
+ LIMIT ${params.limit}
132
+ `.execute(db);
133
+ return results.rows.map((row)=>({
134
+ documentID: row.document_id,
135
+ rank: row.rank
136
+ }));
137
+ }
138
+ }
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, SearchHit, SearchIndexConfig, SearchQuery } from './types.js';
2
+ import type { AbstractAdapter, AdapterTypes, ColumnTypes, Functions, NullOrderingTerm, OrderDirection, SearchHit, SearchIndexConfig, SearchQuery } from './types.js';
3
3
  export type SQLiteTypes = {
4
4
  Binary: Uint8Array;
5
5
  JSON: string;
@@ -14,6 +14,7 @@ export declare class AbstractSQLiteAdapter<T extends AdapterTypes = SQLiteTypes>
14
14
  decodeTimestamp(value: number): Date;
15
15
  coerceFilterValue(value: unknown): unknown;
16
16
  numericCast(expression: Expression<unknown>): Expression<number>;
17
+ nullOrdering(expression: Expression<unknown>, direction: OrderDirection): NullOrderingTerm;
17
18
  readAccessPredicate(params: {
18
19
  viewerDID: string;
19
20
  groupIDs: Array<string>;
package/lib/sqlite.js CHANGED
@@ -1,24 +1,135 @@
1
- import{sql as e}from"kysely";export class AbstractSQLiteAdapter{functions={now:e`(unixepoch())`};types={binary:"blob",json:"jsonb",serial:"integer",text:"text",timestamp:"integer",uuid:"text"};encodeBinary(e){return e}encodeJSON(e){return JSON.stringify(e)}encodeTimestamp(e){return Math.floor(e.getTime()/1e3)}decodeTimestamp(e){return new Date(1e3*e)}coerceFilterValue(e){return"boolean"==typeof e?+!!e:e}numericCast(a){return e`CAST(${a} AS REAL)`}readAccessPredicate(a,s){let{viewerDID:t,groupIDs:r,circleIDs:n,serverDefault:o}=a,d=[e`EXISTS (SELECT 1 FROM json_each(COALESCE(json_extract(data, '$.accessPermissions.read.allowedDIDs'), '[]')) WHERE value = ${t})`];r.length>0&&d.push(e`EXISTS (SELECT 1 FROM json_each(COALESCE(json_extract(data, '$.accessPermissions.read.allowedGroups'), '[]')) WHERE value IN (${e.join(r.map(a=>e`${a}`))}))`),n.length>0&&d.push(e`EXISTS (SELECT 1 FROM json_each(COALESCE(json_extract(data, '$.accessPermissions.read.allowedCircles'), '[]')) WHERE value IN (${e.join(n.map(a=>e`${a}`))}))`);let c=d.reduce((a,s)=>e`${a} OR ${s}`),E=[e`EXISTS (SELECT 1 FROM json_each(COALESCE(_mad.allowed_dids, '[]')) WHERE value = ${t})`];r.length>0&&E.push(e`EXISTS (SELECT 1 FROM json_each(COALESCE(_mad.allowed_groups, '[]')) WHERE value IN (${e.join(r.map(a=>e`${a}`))}))`),n.length>0&&E.push(e`EXISTS (SELECT 1 FROM json_each(COALESCE(_mad.allowed_circles, '[]')) WHERE value IN (${e.join(n.map(a=>e`${a}`))}))`);let i=E.reduce((a,s)=>e`${a} OR ${s}`),l="anyone"===o?e`1 = 1`:"only_owner"===o?e`owner = ${t}`:e`1 = 0`;return e`(
2
- owner = ${t}
1
+ import { sql } from 'kysely';
2
+ export class AbstractSQLiteAdapter {
3
+ functions = {
4
+ now: sql`(unixepoch())`
5
+ };
6
+ types = {
7
+ bigint: 'integer',
8
+ binary: 'blob',
9
+ json: 'jsonb',
10
+ serial: 'integer',
11
+ text: 'text',
12
+ timestamp: 'integer',
13
+ uuid: 'text'
14
+ };
15
+ encodeBinary(value) {
16
+ return value;
17
+ }
18
+ encodeJSON(value) {
19
+ return JSON.stringify(value);
20
+ }
21
+ encodeTimestamp(value) {
22
+ // SQLite timestamps are stored in seconds
23
+ return Math.floor(value.getTime() / 1000);
24
+ }
25
+ decodeTimestamp(value) {
26
+ // SQLite timestamps are stored in seconds
27
+ return new Date(value * 1000);
28
+ }
29
+ coerceFilterValue(value) {
30
+ if (typeof value === 'boolean') {
31
+ return value ? 1 : 0;
32
+ }
33
+ return value;
34
+ }
35
+ numericCast(expression) {
36
+ return sql`CAST(${expression} AS REAL)`;
37
+ }
38
+ nullOrdering(expression, direction) {
39
+ // SQLite has no NULLS LAST/FIRST clause, so prepend an ascending rank column that
40
+ // buckets nulls last on ascending (rank 1 > 0) and first on descending (rank 0 < 1).
41
+ const nullRank = direction === 'asc' ? 1 : 0;
42
+ const valueRank = direction === 'asc' ? 0 : 1;
43
+ return {
44
+ kind: 'lead',
45
+ expression: sql`CASE WHEN ${expression} IS NULL THEN ${sql.lit(nullRank)} ELSE ${sql.lit(valueRank)} END`
46
+ };
47
+ }
48
+ readAccessPredicate(params, modelID) {
49
+ const { viewerDID, groupIDs, circleIDs, serverDefault } = params;
50
+ const docRestrictedParts = [
51
+ sql`EXISTS (SELECT 1 FROM json_each(COALESCE(json_extract(data, '$.accessPermissions.read.allowedDIDs'), '[]')) WHERE value = ${viewerDID})`
52
+ ];
53
+ if (groupIDs.length > 0) {
54
+ docRestrictedParts.push(sql`EXISTS (SELECT 1 FROM json_each(COALESCE(json_extract(data, '$.accessPermissions.read.allowedGroups'), '[]')) WHERE value IN (${sql.join(groupIDs.map((id)=>sql`${id}`))}))`);
55
+ }
56
+ if (circleIDs.length > 0) {
57
+ docRestrictedParts.push(sql`EXISTS (SELECT 1 FROM json_each(COALESCE(json_extract(data, '$.accessPermissions.read.allowedCircles'), '[]')) WHERE value IN (${sql.join(circleIDs.map((id)=>sql`${id}`))}))`);
58
+ }
59
+ const docRestricted = docRestrictedParts.reduce((a, b)=>sql`${a} OR ${b}`);
60
+ const madRestrictedParts = [
61
+ sql`EXISTS (SELECT 1 FROM json_each(COALESCE(_mad.allowed_dids, '[]')) WHERE value = ${viewerDID})`
62
+ ];
63
+ if (groupIDs.length > 0) {
64
+ madRestrictedParts.push(sql`EXISTS (SELECT 1 FROM json_each(COALESCE(_mad.allowed_groups, '[]')) WHERE value IN (${sql.join(groupIDs.map((id)=>sql`${id}`))}))`);
65
+ }
66
+ if (circleIDs.length > 0) {
67
+ madRestrictedParts.push(sql`EXISTS (SELECT 1 FROM json_each(COALESCE(_mad.allowed_circles, '[]')) WHERE value IN (${sql.join(circleIDs.map((id)=>sql`${id}`))}))`);
68
+ }
69
+ const madRestricted = madRestrictedParts.reduce((a, b)=>sql`${a} OR ${b}`);
70
+ const serverDefaultCond = serverDefault === 'anyone' ? sql`1 = 1` : serverDefault === 'only_owner' ? sql`owner = ${viewerDID}` : sql`1 = 0`;
71
+ return sql`(
72
+ owner = ${viewerDID}
3
73
  OR json_extract(data, '$.accessPermissions.read.level') = 'anyone'
4
- OR (json_extract(data, '$.accessPermissions.read.level') = 'only_owner' AND owner = ${t})
5
- OR (json_extract(data, '$.accessPermissions.read.level') = 'restricted' AND (${c}))
74
+ OR (json_extract(data, '$.accessPermissions.read.level') = 'only_owner' AND owner = ${viewerDID})
75
+ OR (json_extract(data, '$.accessPermissions.read.level') = 'restricted' AND (${docRestricted}))
6
76
  OR (json_extract(data, '$.accessPermissions.read.level') IS NULL AND EXISTS (
7
77
  SELECT 1 FROM kubun_graph_user_model_access_defaults AS _mad
8
- WHERE _mad.owner_did = owner AND _mad.model_id = ${s} AND _mad.permission_type = 'read'
78
+ WHERE _mad.owner_did = owner AND _mad.model_id = ${modelID} AND _mad.permission_type = 'read'
79
+ AND (_mad.removed IS NULL OR _mad.removed = 0)
9
80
  AND (
10
81
  _mad.access_level = 'anyone'
11
- OR (_mad.access_level = 'only_owner' AND owner = ${t})
12
- OR (_mad.access_level = 'restricted' AND (${i}))
82
+ OR (_mad.access_level = 'only_owner' AND owner = ${viewerDID})
83
+ OR (_mad.access_level = 'restricted' AND (${madRestricted}))
13
84
  )
14
85
  ))
15
86
  OR (json_extract(data, '$.accessPermissions.read.level') IS NULL AND NOT EXISTS (
16
87
  SELECT 1 FROM kubun_graph_user_model_access_defaults AS _mad2
17
- WHERE _mad2.owner_did = owner AND _mad2.model_id = ${s} AND _mad2.permission_type = 'read'
18
- ) AND (${l}))
19
- )`}async createSearchIndex(a,s){let t=`fts_${s.modelID}`,r=["document_id UNINDEXED",...s.fields].join(", ");await e`CREATE VIRTUAL TABLE IF NOT EXISTS ${e.ref(t)} USING fts5(${e.raw(r)})`.execute(a)}async dropSearchIndex(a,s){let t=`fts_${s}`;await e`DROP TABLE IF EXISTS ${e.ref(t)}`.execute(a)}async updateSearchEntry(a,s,t,r,n){let o=`fts_${s}`;await e`DELETE FROM ${e.ref(o)} WHERE document_id = ${t}`.execute(a);let d=["document_id",...n].join(", "),c=[t,...n.map(e=>r[e]??"")].map(a=>e.lit(a));await e`INSERT INTO ${e.ref(o)} (${e.raw(d)}) VALUES (${e.join(c)})`.execute(a)}async removeSearchEntry(a,s,t){let r=`fts_${s}`;await e`DELETE FROM ${e.ref(r)} WHERE document_id = ${t}`.execute(a)}async searchIndex(a,s){let t=`fts_${s.modelID}`;return(await e`
20
- SELECT document_id, rank FROM ${e.ref(t)}
21
- WHERE ${e.ref(t)} MATCH ${s.query}
88
+ WHERE _mad2.owner_did = owner AND _mad2.model_id = ${modelID} AND _mad2.permission_type = 'read'
89
+ AND (_mad2.removed IS NULL OR _mad2.removed = 0)
90
+ ) AND (${serverDefaultCond}))
91
+ )`;
92
+ }
93
+ async createSearchIndex(db, config) {
94
+ const tableName = `fts_${config.modelID}`;
95
+ const columns = [
96
+ 'document_id UNINDEXED',
97
+ ...config.fields
98
+ ].join(', ');
99
+ await sql`CREATE VIRTUAL TABLE IF NOT EXISTS ${sql.ref(tableName)} USING fts5(${sql.raw(columns)})`.execute(db);
100
+ }
101
+ async dropSearchIndex(db, modelID) {
102
+ const tableName = `fts_${modelID}`;
103
+ await sql`DROP TABLE IF EXISTS ${sql.ref(tableName)}`.execute(db);
104
+ }
105
+ async updateSearchEntry(db, modelID, documentID, fields, fieldNames) {
106
+ const tableName = `fts_${modelID}`;
107
+ await sql`DELETE FROM ${sql.ref(tableName)} WHERE document_id = ${documentID}`.execute(db);
108
+ const columns = [
109
+ 'document_id',
110
+ ...fieldNames
111
+ ].join(', ');
112
+ const values = [
113
+ documentID,
114
+ ...fieldNames.map((f)=>fields[f] ?? '')
115
+ ].map((v)=>sql.lit(v));
116
+ await sql`INSERT INTO ${sql.ref(tableName)} (${sql.raw(columns)}) VALUES (${sql.join(values)})`.execute(db);
117
+ }
118
+ async removeSearchEntry(db, modelID, documentID) {
119
+ const tableName = `fts_${modelID}`;
120
+ await sql`DELETE FROM ${sql.ref(tableName)} WHERE document_id = ${documentID}`.execute(db);
121
+ }
122
+ async searchIndex(db, params) {
123
+ const tableName = `fts_${params.modelID}`;
124
+ const results = await sql`
125
+ SELECT document_id, rank FROM ${sql.ref(tableName)}
126
+ WHERE ${sql.ref(tableName)} MATCH ${params.query}
22
127
  ORDER BY rank
23
- LIMIT ${s.limit}
24
- `.execute(a)).rows.map(e=>({documentID:e.document_id,rank:e.rank}))}}
128
+ LIMIT ${params.limit}
129
+ `.execute(db);
130
+ return results.rows.map((row)=>({
131
+ documentID: row.document_id,
132
+ rank: row.rank
133
+ }));
134
+ }
135
+ }
package/lib/types.d.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  import type { ColumnDataType, ColumnType, Dialect, Expression, Kysely, RawBuilder } from 'kysely';
2
+ export type OrderDirection = 'asc' | 'desc';
3
+ export type NullOrderingTerm = {
4
+ kind: 'lead';
5
+ expression: Expression<number>;
6
+ } | {
7
+ kind: 'native';
8
+ };
2
9
  export type CreatedAtColumn = ColumnType<number, number | undefined, never>;
3
10
  export type UpdatedAtColumn = ColumnType<number, number | undefined, number | undefined>;
4
11
  export type ColumnTypes = {
12
+ bigint: ColumnDataType;
5
13
  binary: ColumnDataType;
6
14
  json: ColumnDataType;
7
15
  serial: ColumnDataType;
@@ -44,6 +52,7 @@ export type AbstractAdapter<T extends AdapterTypes = AdapterTypes> = {
44
52
  searchIndex(db: Kysely<unknown>, params: SearchQuery): Promise<Array<SearchHit>>;
45
53
  coerceFilterValue(value: unknown): unknown;
46
54
  numericCast(expression: Expression<unknown>): Expression<number>;
55
+ nullOrdering(expression: Expression<unknown>, direction: OrderDirection): NullOrderingTerm;
47
56
  readAccessPredicate(params: {
48
57
  viewerDID: string;
49
58
  groupIDs: Array<string>;
package/lib/types.js CHANGED
@@ -1 +1 @@
1
- export{};
1
+ export { };
package/package.json CHANGED
@@ -1,30 +1,30 @@
1
1
  {
2
2
  "name": "@kubun/db-adapter",
3
- "version": "0.10.0",
4
- "license": "see LICENSE.md",
3
+ "version": "0.12.0",
5
4
  "keywords": [],
5
+ "license": "see LICENSE.md",
6
+ "sideEffects": false,
6
7
  "type": "module",
7
- "main": "lib/index.js",
8
- "types": "lib/index.d.ts",
9
8
  "exports": {
10
9
  ".": "./lib/index.js"
11
10
  },
11
+ "main": "lib/index.js",
12
+ "types": "lib/index.d.ts",
12
13
  "files": [
13
14
  "lib/*",
14
15
  "LICENSE.md"
15
16
  ],
16
- "sideEffects": false,
17
17
  "dependencies": {
18
- "kysely": "^0.29.2"
18
+ "kysely": "^0.29.4"
19
19
  },
20
20
  "scripts": {
21
+ "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
21
22
  "build:clean": "del lib",
22
- "build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
23
+ "build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
23
24
  "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
24
25
  "build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
25
- "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
26
+ "test": "pnpm run test:types && pnpm run test:unit",
26
27
  "test:types": "tsc --noEmit",
27
- "test:unit": "vitest run",
28
- "test": "pnpm run test:types && pnpm run test:unit"
28
+ "test:unit": "vitest run"
29
29
  }
30
30
  }