@kubun/db-adapter 0.16.0 → 0.18.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/chunk.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare const IN_LIST_CHUNK = 500;
2
+ export declare function chunked<T>(values: Iterable<T>, size?: number): Array<Array<T>>;
package/lib/chunk.js ADDED
@@ -0,0 +1,15 @@
1
+ // The most values one `IN (...)` list binds; longer lists are read in chunks,
2
+ // well inside every adapter's bind-parameter limit.
3
+ export const IN_LIST_CHUNK = 500;
4
+ export function chunked(values, size = IN_LIST_CHUNK) {
5
+ // Deduplicated first, as a single `IN` would be: a value repeated across
6
+ // two chunks would otherwise return its rows twice.
7
+ const unique = [
8
+ ...new Set(values)
9
+ ];
10
+ const chunks = [];
11
+ for(let start = 0; start < unique.length; start += size){
12
+ chunks.push(unique.slice(start, start + size));
13
+ }
14
+ return chunks;
15
+ }
package/lib/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { chunked, IN_LIST_CHUNK } from './chunk.js';
1
2
  export { AbstractPostgresAdapter, type PostgresTypes } from './postgres.js';
2
3
  export { AbstractSQLiteAdapter, type SQLiteTypes } from './sqlite.js';
3
4
  export type { AbstractAdapter, Adapter, AdapterTypes, ColumnTypes, CreatedAtColumn, Functions, SearchHit, SearchIndexConfig, SearchQuery, UpdatedAtColumn, } from './types.js';
package/lib/index.js CHANGED
@@ -1,2 +1,3 @@
1
+ export { chunked, IN_LIST_CHUNK } from './chunk.js';
1
2
  export { AbstractPostgresAdapter } from './postgres.js';
2
3
  export { AbstractSQLiteAdapter } from './sqlite.js';
package/lib/postgres.d.ts CHANGED
@@ -19,6 +19,10 @@ export declare class AbstractPostgresAdapter<T extends AdapterTypes = PostgresTy
19
19
  arrayIncludesAllPredicate(expression: Expression<unknown>, values: Array<unknown>): RawBuilder<boolean>;
20
20
  arrayIncludesAnyPredicate(expression: Expression<unknown>, values: Array<unknown>): RawBuilder<boolean>;
21
21
  arrayPresencePredicate(expression: Expression<unknown>, mode: ArrayPresenceMode): RawBuilder<boolean>;
22
+ groupReadPredicate(fieldValues: Expression<unknown>, params: {
23
+ groupID: string;
24
+ circleIDs: Array<string>;
25
+ }): RawBuilder<boolean>;
22
26
  readAccessPredicate(params: {
23
27
  viewerDID: string;
24
28
  groupIDs: Array<string>;
package/lib/postgres.js CHANGED
@@ -55,7 +55,7 @@ export class AbstractPostgresAdapter {
55
55
  }
56
56
  arrayIncludesAnyPredicate(expression, values) {
57
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}`))}))`;
58
+ return sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(case when jsonb_typeof(${expression}) = 'array' then ${expression} else '[]'::jsonb end) AS _t WHERE _t IN (${sql.join(values.map((v)=>sql`${v}`))}))`;
59
59
  }
60
60
  arrayPresencePredicate(expression, mode) {
61
61
  // null/nonNull/empty/nonEmpty must see the true NULL, so the node is never coalesced
@@ -75,26 +75,43 @@ export class AbstractPostgresAdapter {
75
75
  throw new Error(`Invalid array presence mode: ${mode}`);
76
76
  }
77
77
  }
78
+ groupReadPredicate(fieldValues, params) {
79
+ const read = sql`${fieldValues} -> '$readRule'`;
80
+ const level = sql`${read} ->> 'level'`;
81
+ const grants = (field, ids)=>{
82
+ const node = sql`${read} -> ${sql.lit(field)}`;
83
+ const array = sql`case when jsonb_typeof(${node}) = 'array' then ${node} else '[]'::jsonb end`;
84
+ return sql`(not exists (select 1 from jsonb_array_elements(${array}) as entry(value) where jsonb_typeof(value) != 'string')
85
+ and exists (select 1 from jsonb_array_elements_text(${array}) as entry(value) where value in (select jsonb_array_elements_text(case when jsonb_typeof(${JSON.stringify(ids)}::text::jsonb) = 'array' then ${JSON.stringify(ids)}::text::jsonb else '[]'::jsonb end))))`;
86
+ };
87
+ const restricted = [
88
+ grants('allowedGroups', [
89
+ params.groupID
90
+ ])
91
+ ];
92
+ if (params.circleIDs.length > 0) restricted.push(grants('allowedCircles', params.circleIDs));
93
+ return sql`(${level} = 'anyone' or (${level} = 'restricted' and (${sql.join(restricted, sql` or `)})))`;
94
+ }
78
95
  readAccessPredicate(params, modelID) {
79
96
  const { viewerDID, groupIDs, circleIDs, serverDefault } = params;
80
97
  const docRestrictedParts = [
81
- sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(data -> 'accessPermissions' -> 'read' -> 'allowedDIDs', '[]'::jsonb)) AS _t WHERE _t = ${viewerDID})`
98
+ sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(case when jsonb_typeof(data -> 'accessPermissions' -> 'read' -> 'allowedDIDs') = 'array' then data -> 'accessPermissions' -> 'read' -> 'allowedDIDs' else '[]'::jsonb end) AS _t WHERE _t = ${viewerDID})`
82
99
  ];
83
100
  if (groupIDs.length > 0) {
84
- 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}`))}))`);
101
+ docRestrictedParts.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(case when jsonb_typeof(data -> 'accessPermissions' -> 'read' -> 'allowedGroups') = 'array' then data -> 'accessPermissions' -> 'read' -> 'allowedGroups' else '[]'::jsonb end) AS _t WHERE _t IN (${sql.join(groupIDs.map((id)=>sql`${id}`))}))`);
85
102
  }
86
103
  if (circleIDs.length > 0) {
87
- 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}`))}))`);
104
+ docRestrictedParts.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(case when jsonb_typeof(data -> 'accessPermissions' -> 'read' -> 'allowedCircles') = 'array' then data -> 'accessPermissions' -> 'read' -> 'allowedCircles' else '[]'::jsonb end) AS _t WHERE _t IN (${sql.join(circleIDs.map((id)=>sql`${id}`))}))`);
88
105
  }
89
106
  const docRestricted = docRestrictedParts.reduce((a, b)=>sql`${a} OR ${b}`);
90
107
  const madRestrictedParts = [
91
- sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(_mad.allowed_dids, '[]'::jsonb)) AS _t WHERE _t = ${viewerDID})`
108
+ sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(case when jsonb_typeof(_mad.allowed_dids) = 'array' then _mad.allowed_dids else '[]'::jsonb end) AS _t WHERE _t = ${viewerDID})`
92
109
  ];
93
110
  if (groupIDs.length > 0) {
94
- 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}`))}))`);
111
+ madRestrictedParts.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(case when jsonb_typeof(_mad.allowed_groups) = 'array' then _mad.allowed_groups else '[]'::jsonb end) AS _t WHERE _t IN (${sql.join(groupIDs.map((id)=>sql`${id}`))}))`);
95
112
  }
96
113
  if (circleIDs.length > 0) {
97
- 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}`))}))`);
114
+ madRestrictedParts.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements_text(case when jsonb_typeof(_mad.allowed_circles) = 'array' then _mad.allowed_circles else '[]'::jsonb end) AS _t WHERE _t IN (${sql.join(circleIDs.map((id)=>sql`${id}`))}))`);
98
115
  }
99
116
  const madRestricted = madRestrictedParts.reduce((a, b)=>sql`${a} OR ${b}`);
100
117
  const serverDefaultCond = serverDefault === 'anyone' ? sql`true` : serverDefault === 'only_owner' ? sql`owner = ${viewerDID}` : sql`false`;
@@ -117,6 +134,9 @@ export class AbstractPostgresAdapter {
117
134
  SELECT 1 FROM kubun_graph_user_model_access_defaults AS _mad2
118
135
  WHERE _mad2.owner_did = owner AND _mad2.model_id = ${modelID} AND _mad2.permission_type = 'read'
119
136
  AND (_mad2.removed IS NULL OR _mad2.removed = 0)
137
+ ) AND NOT EXISTS (
138
+ SELECT 1 FROM kubun_graph_access_default_records AS _adr
139
+ WHERE _adr.type = 'access.default' AND _adr.owner = ${sql.ref(`k_${modelID}.owner`)}
120
140
  ) AND (${serverDefaultCond}))
121
141
  )`;
122
142
  }
package/lib/sqlite.d.ts CHANGED
@@ -19,6 +19,10 @@ export declare class AbstractSQLiteAdapter<T extends AdapterTypes = SQLiteTypes>
19
19
  arrayIncludesAllPredicate(expression: Expression<unknown>, values: Array<unknown>): RawBuilder<boolean>;
20
20
  arrayIncludesAnyPredicate(expression: Expression<unknown>, values: Array<unknown>): RawBuilder<boolean>;
21
21
  arrayPresencePredicate(expression: Expression<unknown>, mode: ArrayPresenceMode): RawBuilder<boolean>;
22
+ groupReadPredicate(fieldValues: Expression<unknown>, params: {
23
+ groupID: string;
24
+ circleIDs: Array<string>;
25
+ }): RawBuilder<boolean>;
22
26
  readAccessPredicate(params: {
23
27
  viewerDID: string;
24
28
  groupIDs: Array<string>;
package/lib/sqlite.js CHANGED
@@ -83,6 +83,22 @@ export class AbstractSQLiteAdapter {
83
83
  throw new Error(`Invalid array presence mode: ${mode}`);
84
84
  }
85
85
  }
86
+ groupReadPredicate(fieldValues, params) {
87
+ const level = sql`json_extract(${fieldValues}, '$."$readRule".level')`;
88
+ const grants = (field, ids)=>{
89
+ const path = `$."$readRule".${field}`;
90
+ return sql`(json_type(${fieldValues}, ${path}) = 'array'
91
+ and not exists (select 1 from json_each(json_extract(${fieldValues}, ${path})) where type != 'text')
92
+ and exists (select 1 from json_each(json_extract(${fieldValues}, ${path})) where value in (select value from json_each(${JSON.stringify(ids)}))))`;
93
+ };
94
+ const restricted = [
95
+ grants('allowedGroups', [
96
+ params.groupID
97
+ ])
98
+ ];
99
+ if (params.circleIDs.length > 0) restricted.push(grants('allowedCircles', params.circleIDs));
100
+ return sql`(${level} = 'anyone' or (${level} = 'restricted' and (${sql.join(restricted, sql` or `)})))`;
101
+ }
86
102
  readAccessPredicate(params, modelID) {
87
103
  const { viewerDID, groupIDs, circleIDs, serverDefault } = params;
88
104
  const docRestrictedParts = [
@@ -125,6 +141,9 @@ export class AbstractSQLiteAdapter {
125
141
  SELECT 1 FROM kubun_graph_user_model_access_defaults AS _mad2
126
142
  WHERE _mad2.owner_did = owner AND _mad2.model_id = ${modelID} AND _mad2.permission_type = 'read'
127
143
  AND (_mad2.removed IS NULL OR _mad2.removed = 0)
144
+ ) AND NOT EXISTS (
145
+ SELECT 1 FROM kubun_graph_access_default_records AS _adr
146
+ WHERE _adr.type = 'access.default' AND _adr.owner = ${sql.ref(`k_${modelID}.owner`)}
128
147
  ) AND (${serverDefaultCond}))
129
148
  )`;
130
149
  }
package/lib/types.d.ts CHANGED
@@ -58,6 +58,10 @@ export type AbstractAdapter<T extends AdapterTypes = AdapterTypes> = {
58
58
  arrayIncludesAllPredicate(expression: Expression<unknown>, values: Array<unknown>): RawBuilder<boolean>;
59
59
  arrayIncludesAnyPredicate(expression: Expression<unknown>, values: Array<unknown>): RawBuilder<boolean>;
60
60
  arrayPresencePredicate(expression: Expression<unknown>, mode: ArrayPresenceMode): RawBuilder<boolean>;
61
+ groupReadPredicate(fieldValues: Expression<unknown>, params: {
62
+ groupID: string;
63
+ circleIDs: Array<string>;
64
+ }): RawBuilder<boolean>;
61
65
  readAccessPredicate(params: {
62
66
  viewerDID: string;
63
67
  groupIDs: Array<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/db-adapter",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "keywords": [],
5
5
  "license": "see LICENSE.md",
6
6
  "sideEffects": false,
@@ -15,7 +15,7 @@
15
15
  "LICENSE.md"
16
16
  ],
17
17
  "dependencies": {
18
- "kysely": "^0.29.5"
18
+ "kysely": "^0.29.7"
19
19
  },
20
20
  "publishConfig": {
21
21
  "access": "public"
@@ -23,8 +23,8 @@
23
23
  "scripts": {
24
24
  "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
25
25
  "build:clean": "del lib",
26
- "build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
27
- "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
26
+ "build:js": "del 'lib/**/*.js' && swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
27
+ "build:types": "del 'lib/**/*.d.ts' 'lib/**/*.d.ts.map' && tsc --emitDeclarationOnly --skipLibCheck",
28
28
  "build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
29
29
  "test": "pnpm run test:types && pnpm run test:unit",
30
30
  "test:types": "tsc --noEmit",