@protobase/query 0.54.6 → 0.55.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/dist/base-query.js +5 -15
- package/dist/estimate-count.js +1 -1
- package/dist/index-coverage.d.ts +8 -4
- package/dist/index-coverage.js +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/read-conditions.d.ts +11 -0
- package/dist/read-conditions.js +35 -0
- package/dist/reads-parent.d.ts +6 -0
- package/dist/reads-parent.js +12 -0
- package/dist/scope.d.ts +16 -1
- package/package.json +2 -2
package/dist/base-query.js
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { tableSource } from "./columns.js";
|
|
2
2
|
import { compileFilter } from "./compile-filter.js";
|
|
3
|
-
import {
|
|
4
|
-
import { sql } from "kysely";
|
|
3
|
+
import { readConditions } from "./read-conditions.js";
|
|
5
4
|
//#region src/base-query.ts
|
|
6
|
-
const softDeleteColumn = (model, name) => {
|
|
7
|
-
const field = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
|
|
8
|
-
return field ? columnRef(field) : sql`${sql.id("t", name)}`;
|
|
9
|
-
};
|
|
10
5
|
/**
|
|
11
|
-
* The table aliased as `t`, minus soft-deleted rows, inside the tenant
|
|
12
|
-
* narrowed by the user's filter. Every query starts here, so no query can see rows the scope hides.
|
|
6
|
+
* The table aliased as `t`, minus soft-deleted rows, inside the tenant, the access rule's row filter and the parents the
|
|
7
|
+
* caller reads, narrowed by the user's filter. Every query starts here, so no query can see rows the scope hides.
|
|
13
8
|
*/
|
|
14
9
|
const baseQuery = (db, model, filter, scope) => {
|
|
15
|
-
const full = fullModelOf(model, scope);
|
|
16
|
-
const softDelete = full.softDelete ?? model.softDelete;
|
|
17
10
|
let query = db.selectFrom(tableSource(model));
|
|
18
|
-
|
|
19
|
-
const tenant = tenantCondition(model, scope);
|
|
20
|
-
if (tenant) query = query.where(tenant);
|
|
21
|
-
if (scope?.rowFilter) query = query.where(compileFilter(full, scope.rowFilter));
|
|
11
|
+
for (const condition of readConditions(model, scope)) query = query.where(condition);
|
|
22
12
|
if (filter) query = query.where(compileFilter(model, filter));
|
|
23
13
|
return query;
|
|
24
14
|
};
|
package/dist/estimate-count.js
CHANGED
|
@@ -27,7 +27,7 @@ const tableReltuples = async (db, model) => {
|
|
|
27
27
|
*/
|
|
28
28
|
const estimateCount = async (db, model, filter, scope) => {
|
|
29
29
|
const full = fullModelOf(model, scope);
|
|
30
|
-
if (filter || scope?.rowFilter || full.softDelete || full.tenant || model.softDelete || model.tenant) return planEstimate(db, model, filter, scope);
|
|
30
|
+
if (filter || scope?.rowFilter || scope?.parents?.length || full.softDelete || full.tenant || model.softDelete || model.tenant) return planEstimate(db, model, filter, scope);
|
|
31
31
|
const reltuples = await tableReltuples(db, model);
|
|
32
32
|
return reltuples >= 0 ? Math.round(reltuples) : planEstimate(db, model);
|
|
33
33
|
};
|
package/dist/index-coverage.d.ts
CHANGED
|
@@ -8,8 +8,11 @@ import { CheckedFilter, ResourceModel, SortSpec } from "@protobase/schema";
|
|
|
8
8
|
* column's digits (`regexp_replace(column, '[^0-9]', '', 'g')`) for a `digitsEnd` search field; `gin` for JSON.
|
|
9
9
|
*/
|
|
10
10
|
export type IndexNeed = 'btree' | 'trigram' | 'digits' | 'gin';
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Where a column is read: the caller's filter, the access rule's row filter, the relation to a parent the caller must
|
|
13
|
+
* read (`.parent(fk, 'read')`), the sort, or a facet's grouping.
|
|
14
|
+
*/
|
|
15
|
+
export type ColumnUse = 'filter' | 'access' | 'parent' | 'sort' | 'facet';
|
|
13
16
|
/** A column read without an index that serves it, and a statement that adds one. */
|
|
14
17
|
export type UncoveredColumn = {
|
|
15
18
|
field: string;
|
|
@@ -29,8 +32,9 @@ export declare const indexServes: (index: TableIndex, column: string, need: Inde
|
|
|
29
32
|
/**
|
|
30
33
|
* The columns a read of the resource would read without an index serving them; empty when every one is served. The
|
|
31
34
|
* filter's and the access row filter's columns each need an index of the kind their operator uses, the sort's first
|
|
32
|
-
* field
|
|
33
|
-
*
|
|
35
|
+
* field, a grouped field and each relation to a parent the caller must read a btree index; the parent's own reads are
|
|
36
|
+
* judged against its table by a call of their own. Tenant, soft delete and `=` terms pin columns, so a later key of a
|
|
37
|
+
* btree index serves under them. A plain view or a foreign table has no indexes of its own and is not judged.
|
|
34
38
|
*/
|
|
35
39
|
export declare const uncoveredColumns: (model: ResourceModel, table: TableIndexes, query: CoverageQuery, scope?: Scope) => UncoveredColumn[];
|
|
36
40
|
//#endregion
|
package/dist/index-coverage.js
CHANGED
|
@@ -76,8 +76,9 @@ const unique = (columns) => columns.filter((column, i) => columns.findIndex((oth
|
|
|
76
76
|
/**
|
|
77
77
|
* The columns a read of the resource would read without an index serving them; empty when every one is served. The
|
|
78
78
|
* filter's and the access row filter's columns each need an index of the kind their operator uses, the sort's first
|
|
79
|
-
* field
|
|
80
|
-
*
|
|
79
|
+
* field, a grouped field and each relation to a parent the caller must read a btree index; the parent's own reads are
|
|
80
|
+
* judged against its table by a call of their own. Tenant, soft delete and `=` terms pin columns, so a later key of a
|
|
81
|
+
* btree index serves under them. A plain view or a foreign table has no indexes of its own and is not judged.
|
|
81
82
|
*/
|
|
82
83
|
const uncoveredColumns = (model, table, query, scope) => {
|
|
83
84
|
if (table.kind !== "table") return [];
|
|
@@ -98,6 +99,7 @@ const uncoveredColumns = (model, table, query, scope) => {
|
|
|
98
99
|
const groupField = query.groupBy !== void 0 ? model.fields[query.groupBy] : void 0;
|
|
99
100
|
return unique([
|
|
100
101
|
...scope?.rowFilter ? walk(ctx("access", full), scope.rowFilter, pinned, false) : [],
|
|
102
|
+
...(scope?.parents ?? []).flatMap((read) => need(ctx("parent", full), read.field, "btree", pinned)),
|
|
101
103
|
...query.filter ? walk(ctx("filter"), query.filter, pinned, false) : [],
|
|
102
104
|
...sortField ? need(ctx("sort"), sortField, "btree", pinned) : [],
|
|
103
105
|
...groupField ? need(ctx("facet"), groupField, "btree", pinned) : []
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CompileOptions, compileFilter } from "./compile-filter.js";
|
|
2
2
|
import { Db } from "./db.js";
|
|
3
|
-
import { Scope, tenantCondition } from "./scope.js";
|
|
3
|
+
import { ParentRead, Scope, tenantCondition } from "./scope.js";
|
|
4
4
|
import { RelatedRowsQuery, buildRelatedRows, relatedRows } from "./related-rows.js";
|
|
5
5
|
import { ExtraColumn, ListOptions, ListPlan, ListResult, buildListQuery, listQuery, maxLimit } from "./list-query.js";
|
|
6
6
|
import { estimateCount } from "./estimate-count.js";
|
|
@@ -19,5 +19,7 @@ import { ScrollMode, scrollMode } from "./scroll-mode.js";
|
|
|
19
19
|
import { IndexKey, TableIndex, TableIndexes, tableIndexes } from "./table-indexes.js";
|
|
20
20
|
import { ColumnUse, CoverageQuery, IndexNeed, UncoveredColumn, indexServes, uncoveredColumns } from "./index-coverage.js";
|
|
21
21
|
import { QueryError, QueryErrorCode } from "./fields/query-error.js";
|
|
22
|
+
import { parentCondition } from "./read-conditions.js";
|
|
23
|
+
import { readsParent } from "./reads-parent.js";
|
|
22
24
|
import { SeekRequest, seekToPosition } from "./seek-to-position.js";
|
|
23
|
-
export { type ColumnUse, type CompileOptions, type CoverageQuery, type Db, type ExtraColumn, type FacetCount, type FacetOptions, type Granularity, type GroupTotal, type GroupTotalsOptions, type Histogram, type HistogramBucket, type IndexKey, type IndexNeed, type ListOptions, type ListPlan, type ListResult, type MapBounds, type MapBucket, type MapBuckets, type MapPosition, type Measure, QueryError, type QueryErrorCode, type RelatedRowsQuery, type Scope, type ScrollMode, type SeekRequest, type SeriesOptions, type SeriesPoint, type SplitPoint, type SplitSeriesOptions, type TableIndex, type TableIndexes, type UncoveredColumn, buildListQuery, buildRelatedRows, cellsAcross, checkMapPosition, compileFilter, countUpTo, estimateCount, exactCount, facetCounts, groupTotals, histogram, indexServes, listQuery, mapBuckets, mapCellPixels, maxLimit, maxMapBuckets, maxMapZoom, relatedRows, scrollMode, seekToPosition, series, splitSeries, tableIndexes, tenantCondition, uncoveredColumns, withoutField };
|
|
25
|
+
export { type ColumnUse, type CompileOptions, type CoverageQuery, type Db, type ExtraColumn, type FacetCount, type FacetOptions, type Granularity, type GroupTotal, type GroupTotalsOptions, type Histogram, type HistogramBucket, type IndexKey, type IndexNeed, type ListOptions, type ListPlan, type ListResult, type MapBounds, type MapBucket, type MapBuckets, type MapPosition, type Measure, type ParentRead, QueryError, type QueryErrorCode, type RelatedRowsQuery, type Scope, type ScrollMode, type SeekRequest, type SeriesOptions, type SeriesPoint, type SplitPoint, type SplitSeriesOptions, type TableIndex, type TableIndexes, type UncoveredColumn, buildListQuery, buildRelatedRows, cellsAcross, checkMapPosition, compileFilter, countUpTo, estimateCount, exactCount, facetCounts, groupTotals, histogram, indexServes, listQuery, mapBuckets, mapCellPixels, maxLimit, maxMapBuckets, maxMapZoom, parentCondition, readsParent, relatedRows, scrollMode, seekToPosition, series, splitSeries, tableIndexes, tenantCondition, uncoveredColumns, withoutField };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { QueryError } from "./fields/query-error.js";
|
|
2
2
|
import { compileFilter } from "./compile-filter.js";
|
|
3
3
|
import { tenantCondition } from "./scope.js";
|
|
4
|
+
import { parentCondition } from "./read-conditions.js";
|
|
4
5
|
import { buildListQuery, listQuery, maxLimit } from "./list-query.js";
|
|
5
6
|
import { buildRelatedRows, relatedRows } from "./related-rows.js";
|
|
6
7
|
import { estimateCount } from "./estimate-count.js";
|
|
@@ -16,5 +17,6 @@ import { cellsAcross, checkMapPosition, mapBuckets, mapCellPixels, maxMapBuckets
|
|
|
16
17
|
import { scrollMode } from "./scroll-mode.js";
|
|
17
18
|
import { tableIndexes } from "./table-indexes.js";
|
|
18
19
|
import { indexServes, uncoveredColumns } from "./index-coverage.js";
|
|
20
|
+
import { readsParent } from "./reads-parent.js";
|
|
19
21
|
import { seekToPosition } from "./seek-to-position.js";
|
|
20
|
-
export { QueryError, buildListQuery, buildRelatedRows, cellsAcross, checkMapPosition, compileFilter, countUpTo, estimateCount, exactCount, facetCounts, groupTotals, histogram, indexServes, listQuery, mapBuckets, mapCellPixels, maxLimit, maxMapBuckets, maxMapZoom, relatedRows, scrollMode, seekToPosition, series, splitSeries, tableIndexes, tenantCondition, uncoveredColumns, withoutField };
|
|
22
|
+
export { QueryError, buildListQuery, buildRelatedRows, cellsAcross, checkMapPosition, compileFilter, countUpTo, estimateCount, exactCount, facetCounts, groupTotals, histogram, indexServes, listQuery, mapBuckets, mapCellPixels, maxLimit, maxMapBuckets, maxMapZoom, parentCondition, readsParent, relatedRows, scrollMode, seekToPosition, series, splitSeries, tableIndexes, tenantCondition, uncoveredColumns, withoutField };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ParentRead } from "./scope.js";
|
|
2
|
+
import { Expression, SqlBool } from "kysely";
|
|
3
|
+
import { ResourceModel } from "@protobase/schema";
|
|
4
|
+
//#region src/read-conditions.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* `fk in (select key from parent where <the caller's scope on it>)`, or `false` when the caller reads no parent; a
|
|
7
|
+
* nullable `fk` also keeps the rows that name no parent. The subquery aliases the parent `t` too, so its conditions read
|
|
8
|
+
* the parent's columns and only `fk` reads the outer row.
|
|
9
|
+
*/
|
|
10
|
+
export declare const parentCondition: (read: ParentRead) => Expression<SqlBool>;
|
|
11
|
+
//#endregion
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { columnRef, tableSource } from "./columns.js";
|
|
2
|
+
import { compileFilter } from "./compile-filter.js";
|
|
3
|
+
import { fullModelOf, tenantCondition } from "./scope.js";
|
|
4
|
+
import { sql } from "kysely";
|
|
5
|
+
//#region src/read-conditions.ts
|
|
6
|
+
const softDeleteColumn = (model, name) => {
|
|
7
|
+
const field = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
|
|
8
|
+
return field ? columnRef(field) : sql`${sql.id("t", name)}`;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* `fk in (select key from parent where <the caller's scope on it>)`, or `false` when the caller reads no parent; a
|
|
12
|
+
* nullable `fk` also keeps the rows that name no parent. The subquery aliases the parent `t` too, so its conditions read
|
|
13
|
+
* the parent's columns and only `fk` reads the outer row.
|
|
14
|
+
*/
|
|
15
|
+
const parentCondition = (read) => {
|
|
16
|
+
const column = columnRef(read.field);
|
|
17
|
+
const { parent } = read;
|
|
18
|
+
const within = parent ? sql`${column} in (select ${columnRef(parent.key)} from ${tableSource(parent.model)}${whereAll(readConditions(parent.model, parent.scope))})` : sql`false`;
|
|
19
|
+
return read.field.nullable ? sql`(${column} is null or ${within})` : within;
|
|
20
|
+
};
|
|
21
|
+
const whereAll = (conditions) => conditions.length === 0 ? sql`` : sql` where ${sql.join(conditions, sql` and `)}`;
|
|
22
|
+
/** What the scope hides of the table aliased as `t`: soft-deleted rows, other tenants, the row filter and unread parents. */
|
|
23
|
+
const readConditions = (model, scope) => {
|
|
24
|
+
const full = fullModelOf(model, scope);
|
|
25
|
+
const softDelete = full.softDelete ?? model.softDelete;
|
|
26
|
+
const tenant = tenantCondition(model, scope);
|
|
27
|
+
return [
|
|
28
|
+
...softDelete ? [sql`${softDeleteColumn(full, softDelete)} is null`] : [],
|
|
29
|
+
...tenant ? [tenant] : [],
|
|
30
|
+
...scope?.rowFilter ? [compileFilter(full, scope.rowFilter)] : [],
|
|
31
|
+
...(scope?.parents ?? []).map(parentCondition)
|
|
32
|
+
];
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { parentCondition, readConditions };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Db } from "./db.js";
|
|
2
|
+
import { ParentRead } from "./scope.js";
|
|
3
|
+
//#region src/reads-parent.d.ts
|
|
4
|
+
/** Whether the caller reads the parent record whose key is `value`, by the same scope a read of its children keeps to. */
|
|
5
|
+
export declare const readsParent: (db: Db, read: ParentRead, value: string | number) => Promise<boolean>;
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { columnRef } from "./columns.js";
|
|
2
|
+
import { baseQuery } from "./base-query.js";
|
|
3
|
+
import { sql } from "kysely";
|
|
4
|
+
//#region src/reads-parent.ts
|
|
5
|
+
/** Whether the caller reads the parent record whose key is `value`, by the same scope a read of its children keeps to. */
|
|
6
|
+
const readsParent = async (db, read, value) => {
|
|
7
|
+
if (!read.parent) return false;
|
|
8
|
+
const { model, key, scope } = read.parent;
|
|
9
|
+
return await baseQuery(db, model, void 0, scope).select(sql`1`.as("one")).where(sql`${columnRef(key)} = ${value}`).limit(1).executeTakeFirst() !== void 0;
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { readsParent };
|
package/dist/scope.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Expression, SqlBool } from "kysely";
|
|
2
|
-
import { CheckedFilter, ResourceModel } from "@protobase/schema";
|
|
2
|
+
import { CheckedFilter, FieldModel, ResourceModel } from "@protobase/schema";
|
|
3
3
|
//#region src/scope.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Who is asking, and which rows they may see.
|
|
@@ -8,6 +8,8 @@ import { CheckedFilter, ResourceModel } from "@protobase/schema";
|
|
|
8
8
|
* each tenant to its own access. An empty list sees no rows.
|
|
9
9
|
* - `rowFilter`: a filter from access rules, ANDed in before the user's filter. It is checked against the FULL model,
|
|
10
10
|
* so it may use hidden or non-filterable columns the user's own (restricted) model does not expose.
|
|
11
|
+
* - `parents`: the relations whose record each row belongs to (`.parent(fk, 'read')`), keeping the rows whose parent the
|
|
12
|
+
* caller reads; ANDed in with `rowFilter`.
|
|
11
13
|
* - `fullModel`: that full model. Soft delete, tenant and `rowFilter` resolve against it; everything the user
|
|
12
14
|
* supplies (filter, sort, columns, fields) resolves against the model argument. Defaults to the model argument.
|
|
13
15
|
*/
|
|
@@ -15,8 +17,21 @@ export type Scope = {
|
|
|
15
17
|
tenantValue?: string | number;
|
|
16
18
|
tenantValues?: readonly (string | number)[];
|
|
17
19
|
rowFilter?: CheckedFilter;
|
|
20
|
+
parents?: readonly ParentRead[];
|
|
18
21
|
fullModel?: ResourceModel;
|
|
19
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* A relation whose parent the caller must read for a row to be seen: `field` of the full model, and the parent's full
|
|
25
|
+
* model, key and the caller's scope on it; without `parent` the caller reads no parent, so only a null `field` is seen.
|
|
26
|
+
*/
|
|
27
|
+
export type ParentRead = {
|
|
28
|
+
field: FieldModel;
|
|
29
|
+
parent?: {
|
|
30
|
+
model: ResourceModel;
|
|
31
|
+
key: FieldModel;
|
|
32
|
+
scope: Scope;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
20
35
|
/**
|
|
21
36
|
* `tenant = value`, or `tenant in (values)`, for tenant models; throws when the scope is missing, so a forgotten scope
|
|
22
37
|
* never leaks rows.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/query",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
4
4
|
"description": "Kysely list, filter, count and aggregate queries over Protobase resources.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@protobase/schema": "0.
|
|
32
|
+
"@protobase/schema": "0.55.0",
|
|
33
33
|
"kysely": "^0.29.6"
|
|
34
34
|
}
|
|
35
35
|
}
|