@protobase/server 0.17.0 → 0.19.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/admin-options.d.ts +1 -2
- package/dist/aggregate-service.js +30 -19
- package/dist/check-view-queries.d.ts +5 -0
- package/dist/check-view-queries.js +41 -0
- package/dist/check-views.js +16 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/list-photos.js +1 -2
- package/dist/list-service.js +4 -3
- package/dist/map-service.js +1 -1
- package/dist/openapi/components.js +1 -1
- package/dist/openapi/resource-paths.js +1 -1
- package/dist/routes/collection.js +4 -1
- package/dist/scan-guard.js +30 -57
- package/dist/view-access.js +19 -4
- package/package.json +4 -4
- package/dist/filter-shape.js +0 -25
package/dist/admin-options.d.ts
CHANGED
|
@@ -13,10 +13,9 @@ export type AdminOptions = {
|
|
|
13
13
|
basePath?: string;
|
|
14
14
|
/** Applied to every request's transaction with SET LOCAL. Default 15000. */
|
|
15
15
|
statementTimeoutMs?: number;
|
|
16
|
-
/**
|
|
16
|
+
/** Reads that filter, sort or count values by a column no index serves. Default mode `reject`. */
|
|
17
17
|
scanGuard?: {
|
|
18
18
|
mode?: ScanGuardMode;
|
|
19
|
-
seqScanRows?: number;
|
|
20
19
|
};
|
|
21
20
|
/** Runs inside the write transaction after each create, update and delete. */
|
|
22
21
|
writeHooks?: PipelineHook[];
|
|
@@ -5,7 +5,7 @@ import { readScope, requestAccess, requireOperation } from "./request-access.js"
|
|
|
5
5
|
import { readTransaction } from "./transactions.js";
|
|
6
6
|
import { parseParams } from "./params.js";
|
|
7
7
|
import { labelRequests, relatedLabels } from "./related-labels.js";
|
|
8
|
-
import { facetCounts, histogram, series } from "@protobase/query";
|
|
8
|
+
import { facetCounts, histogram, series, withoutField } from "@protobase/query";
|
|
9
9
|
import { sql } from "kysely";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
//#region src/aggregate-service.ts
|
|
@@ -66,25 +66,36 @@ const facets = async (deps, entry, session, input) => {
|
|
|
66
66
|
const params = parseParams(facetParams, input);
|
|
67
67
|
const { model, filter: checked, scope } = await scoped(deps, entry, session, params.filter);
|
|
68
68
|
const labels = params.label === void 0 ? [] : await labelRequests(deps, session, model, [params.field], `${params.field}.${params.label}`);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const named = counts.map((facet) => {
|
|
77
|
-
const label = facet.value === null ? void 0 : names[String(facet.value)];
|
|
78
|
-
return label === void 0 ? facet : {
|
|
79
|
-
...facet,
|
|
80
|
-
label
|
|
81
|
-
};
|
|
82
|
-
});
|
|
83
|
-
return {
|
|
84
|
-
field: params.field,
|
|
85
|
-
facets: named
|
|
86
|
-
};
|
|
69
|
+
const others = withoutField(checked, params.field);
|
|
70
|
+
const warning = await deps.guard(entry, {
|
|
71
|
+
...others && { filter: others },
|
|
72
|
+
groupBy: params.field
|
|
73
|
+
}, {
|
|
74
|
+
model,
|
|
75
|
+
scope
|
|
87
76
|
});
|
|
77
|
+
return {
|
|
78
|
+
body: await readTransaction(deps.db, deps.statementTimeoutMs, async (trx) => {
|
|
79
|
+
const counts = await facetCounts(trx, model, params.field, checked, {
|
|
80
|
+
statementTimeoutMs: deps.statementTimeoutMs,
|
|
81
|
+
...params.limit !== void 0 && { limit: params.limit },
|
|
82
|
+
scope
|
|
83
|
+
});
|
|
84
|
+
const names = (await relatedLabels(trx, labels, counts.map((facet) => ({ [params.field]: facet.value }))))[params.field] ?? {};
|
|
85
|
+
const named = counts.map((facet) => {
|
|
86
|
+
const label = facet.value === null ? void 0 : names[String(facet.value)];
|
|
87
|
+
return label === void 0 ? facet : {
|
|
88
|
+
...facet,
|
|
89
|
+
label
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
field: params.field,
|
|
94
|
+
facets: named
|
|
95
|
+
};
|
|
96
|
+
}),
|
|
97
|
+
warning
|
|
98
|
+
};
|
|
88
99
|
};
|
|
89
100
|
const earliest = async (deps, access, fieldName, scope) => {
|
|
90
101
|
const target = access.model.fields[fieldName];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ResourceModel, ViewModel } from "@protobase/schema";
|
|
2
|
+
//#region src/check-view-queries.d.ts
|
|
3
|
+
/** Throws one error listing what every view asks for that its resource does not allow; views of unknown resources are left to the other checks. */
|
|
4
|
+
export declare const checkViewQueries: (models: ResourceModel[], views: ViewModel[]) => void;
|
|
5
|
+
//#endregion
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/check-view-queries.ts
|
|
2
|
+
const widgetName = {
|
|
3
|
+
facets: "facets filter",
|
|
4
|
+
range: "range filter",
|
|
5
|
+
dateRange: "date range filter",
|
|
6
|
+
toggle: "toggle",
|
|
7
|
+
flags: "flags filter"
|
|
8
|
+
};
|
|
9
|
+
const filterableErrors = (model, names, what) => names.flatMap((name) => {
|
|
10
|
+
const field = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
|
|
11
|
+
if (!field) return [`${what} names "${name}", which is not a field of ${model.name}`];
|
|
12
|
+
return field.filterable ? [] : [`${what} uses "${name}", which is not filterable, so the API refuses it: mark "${name}" .filterable()`];
|
|
13
|
+
});
|
|
14
|
+
const listSortErrors = (model, view) => (view.list?.sort ?? []).flatMap(([name]) => {
|
|
15
|
+
const field = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
|
|
16
|
+
if (!field) return [`the list sorts by "${name}", which is not a field of ${model.name}`];
|
|
17
|
+
return field.sortable ? [] : [`the list sorts by "${name}", which is not sortable, so the API refuses it: mark "${name}" .sortable()`];
|
|
18
|
+
});
|
|
19
|
+
const searchErrors = (model, view) => (view.list?.search ?? []).filter((name) => !(model.search ?? []).includes(name)).map((name) => `the list searches "${name}", which ${model.name} does not search: add it to the resource's .search(...)`);
|
|
20
|
+
/**
|
|
21
|
+
* What a view asks the API for that its resource does not allow, which the API refuses only once a page asks: a filter
|
|
22
|
+
* widget or the chart on a field that is not `.filterable()`, a list sorted by one that is not `.sortable()`, and a list
|
|
23
|
+
* search on a field the resource does not search. The server's startup checks the rest of a view.
|
|
24
|
+
*/
|
|
25
|
+
const viewQueryErrors = (view, model) => [
|
|
26
|
+
...view.filters.flatMap((widget) => filterableErrors(model, widget.kind === "flags" ? widget.fields : [widget.field], `the ${widgetName[widget.kind]}`)),
|
|
27
|
+
...view.chart ? filterableErrors(model, [view.chart.field], "the chart") : [],
|
|
28
|
+
...listSortErrors(model, view),
|
|
29
|
+
...searchErrors(model, view)
|
|
30
|
+
];
|
|
31
|
+
/** Throws one error listing what every view asks for that its resource does not allow; views of unknown resources are left to the other checks. */
|
|
32
|
+
const checkViewQueries = (models, views) => {
|
|
33
|
+
const byName = new Map(models.map((model) => [model.name, model]));
|
|
34
|
+
const problems = views.flatMap((view) => {
|
|
35
|
+
const model = byName.get(view.resource);
|
|
36
|
+
return model ? viewQueryErrors(view, model).map((problem) => `View "${view.resource}": ${problem}`) : [];
|
|
37
|
+
});
|
|
38
|
+
if (problems.length > 0) throw new Error(`The views ask for what their resources do not allow:\n${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { checkViewQueries, viewQueryErrors };
|
package/dist/check-views.js
CHANGED
|
@@ -21,6 +21,7 @@ const sensitiveErrors = (view, model) => {
|
|
|
21
21
|
["the chart", view.chart ? [view.chart.field] : []],
|
|
22
22
|
["the sidebar", view.layout.flatMap((item) => item.kind === "sidebar" ? item.fields : [])],
|
|
23
23
|
["a map", view.layout.flatMap((item) => item.kind === "map" ? [item.latitude, item.longitude] : [])],
|
|
24
|
+
["a gallery", view.layout.flatMap((item) => item.kind === "gallery" && item.image !== void 0 ? [item.image] : [])],
|
|
24
25
|
["the recent records", view.nav?.recent ? [view.nav.recent.status] : []],
|
|
25
26
|
["an action", view.actions.flatMap((action) => action.run?.kind === "update" ? Object.keys(action.run.values) : linkFields(action.run))]
|
|
26
27
|
].flatMap(([place, names]) => [...new Set(names.filter(sensitive))].map((name) => `sensitive field "${name}" cannot be in ${place}`));
|
|
@@ -55,7 +56,11 @@ const linkErrors = (view, model, models, pages) => view.actions.flatMap(({ name,
|
|
|
55
56
|
});
|
|
56
57
|
const unknownFieldErrors = (view, model) => {
|
|
57
58
|
const unknown = (names, what) => names.filter((name) => !Object.hasOwn(model.fields, name)).map((name) => `${what} names field "${name}", which ${model.name} does not have`);
|
|
58
|
-
return [
|
|
59
|
+
return [
|
|
60
|
+
...(view.list?.displays ?? []).flatMap((display) => unknown(displayFields(display), `display "${display.name}"`)),
|
|
61
|
+
...view.layout.flatMap((item) => item.kind === "custom" ? unknown(fieldRefsIn(item.props), `custom section ${item.component}`) : []),
|
|
62
|
+
...view.layout.flatMap((item) => item.kind === "gallery" && item.image !== void 0 ? unknown([item.image], `gallery "${item.title}"`) : [])
|
|
63
|
+
];
|
|
59
64
|
};
|
|
60
65
|
const widgetErrors = (view, model, fieldTypes) => Object.entries(view.fields).flatMap(([name, hints]) => {
|
|
61
66
|
const of = hints.widget && fieldTypes[hints.widget.name];
|
|
@@ -71,6 +76,10 @@ const sharedPhotosErrors = (view) => {
|
|
|
71
76
|
const same = (a, b) => a.resource === b.resource && a.field === b.field && a.image === b.image && a.sort === b.sort;
|
|
72
77
|
return shown.filter(({ photos }) => !same(photos, shown[0].photos)).map(({ name }) => `display "${name}" photos: the list's records load with one set of photos, so name those of display "${shown[0].name}"`);
|
|
73
78
|
};
|
|
79
|
+
const galleryErrors = (item, resource, model, models) => {
|
|
80
|
+
const image = item.image === void 0 ? void 0 : model.fields[item.image];
|
|
81
|
+
return [...image && image.type !== "file" ? [`"${item.image}" is not a file field`] : [], ...item.photos ? cardPhotosErrors(item.photos, resource, models).map((error) => `photos: ${error}`) : []];
|
|
82
|
+
};
|
|
74
83
|
const listOfErrors = (model, name, models) => {
|
|
75
84
|
const target = models[model.fields[name]?.relation?.resource ?? ""];
|
|
76
85
|
if (!target) return [`"${name}" points at no resource of the app`];
|
|
@@ -80,7 +89,7 @@ const listOfErrors = (model, name, models) => {
|
|
|
80
89
|
* Fails at startup when a view uses a sensitive field where it would show empty, a display or custom section names a
|
|
81
90
|
* field the resource does not have, labels or colours values its enum does not have, shows a field with a field type of
|
|
82
91
|
* `fieldTypes` (each with the storage types it shows) that does not show it,
|
|
83
|
-
* has a related section, a cards display's photos or favorite button, a `listOf`, a "Save search" button or a
|
|
92
|
+
* has a related section, a cards display's or gallery's photos or a cards display's favorite button, a `listOf`, a "Save search" button or a
|
|
84
93
|
* `searchesOf` that do not fit the resources, cards
|
|
85
94
|
* displays of one list that name different photos, a map that cannot show the
|
|
86
95
|
* record's position or a map display that cannot find the records in its view, or a link to an address the app does not have.
|
|
@@ -92,7 +101,11 @@ const checkViews = (registry, views, pages = [], fieldTypes = {}) => {
|
|
|
92
101
|
const model = models[view.resource];
|
|
93
102
|
if (!model) return [];
|
|
94
103
|
const related = view.layout.flatMap((item) => item.kind === "related" ? relatedErrors(item, view.resource, models).map((error) => `related "${item.title}": ${error}`) : []);
|
|
95
|
-
const photos = [
|
|
104
|
+
const photos = [
|
|
105
|
+
...(view.list?.displays ?? []).flatMap((display) => display.kind === "cards" && display.photos ? cardPhotosErrors(display.photos, view.resource, models).map((error) => `display "${display.name}" photos: ${error}`) : []),
|
|
106
|
+
...sharedPhotosErrors(view),
|
|
107
|
+
...view.layout.flatMap((item) => item.kind === "gallery" ? galleryErrors(item, view.resource, model, models).map((error) => `gallery "${item.title}": ${error}`) : [])
|
|
108
|
+
];
|
|
96
109
|
const favorites = (view.list?.displays ?? []).flatMap((display) => display.kind === "cards" && display.favorite ? [...cardFavoriteErrors(display.favorite, view.resource, models), ...favoriteOwnerErrors(display.favorite, models)].map((error) => `display "${display.name}" favorite: ${error}`) : []);
|
|
97
110
|
const listOf = view.listOf === void 0 ? [] : listOfErrors(model, view.listOf, models).map((error) => `listOf: ${error}`);
|
|
98
111
|
const searches = [...view.saveSearch ? saveSearchErrors(view.saveSearch, view.resource, models, views).map((error) => `saveSearch: ${error}`) : [], ...view.searchesOf ? searchesOfErrors(view.searchesOf, view.resource, models).map((error) => `searchesOf: ${error}`) : []];
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { MapOptions, mapStyleUrl, mapStyleVariable } from "./maps/map-settings.j
|
|
|
37
37
|
import { RuntimeOptions, runtimeUrl, runtimeVariable, seesRuntime } from "./runtime/runtime-settings.js";
|
|
38
38
|
import { AdminOptions } from "./admin-options.js";
|
|
39
39
|
import { CreateAdminInput, createAdmin } from "./create-admin.js";
|
|
40
|
+
import { checkViewQueries } from "./check-view-queries.js";
|
|
40
41
|
import { ProjectConfig, defineConfig, mergeConfig } from "./project-config.js";
|
|
41
42
|
import { consoleAuditQueue } from "./audit/console-queue.js";
|
|
42
43
|
import { JwtOptions, jwtAuthenticator } from "./auth/jwt.js";
|
|
@@ -82,4 +83,4 @@ import { FatalJobError } from "./jobs/run-job.js";
|
|
|
82
83
|
import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/runtime.js";
|
|
83
84
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
84
85
|
import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
|
|
85
|
-
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapOptions, type MemberInput, MissingCredentials, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkSourceNames, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storeFile, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
|
86
|
+
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapOptions, type MemberInput, MissingCredentials, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storeFile, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,7 @@ import { checkSourceNames, sourceOptions, sourcesFor, sourcesVariable } from "./
|
|
|
42
42
|
import { renderVariant, thumbHash } from "./files/images.js";
|
|
43
43
|
import { createAdmin } from "./create-admin.js";
|
|
44
44
|
import { configExports } from "./resource-source.js";
|
|
45
|
+
import { checkViewQueries } from "./check-view-queries.js";
|
|
45
46
|
import { defineConfig, mergeConfig } from "./project-config.js";
|
|
46
47
|
import { jwtAuthenticator } from "./auth/jwt.js";
|
|
47
48
|
import { withTrustedApps } from "./better-auth/app-tokens.js";
|
|
@@ -63,4 +64,4 @@ import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/p
|
|
|
63
64
|
import { defineEmail } from "./mail/define-email.js";
|
|
64
65
|
import { startJobs } from "./jobs/runtime.js";
|
|
65
66
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
66
|
-
export { FatalJobError, HttpProblem, MissingCredentials, ModelError, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkSourceNames, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storeFile, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
|
67
|
+
export { FatalJobError, HttpProblem, MissingCredentials, ModelError, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storeFile, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
package/dist/list-photos.js
CHANGED
|
@@ -69,8 +69,7 @@ const recordPhotos = async (deps, request, model, rows) => {
|
|
|
69
69
|
const scope = readScope(access);
|
|
70
70
|
const warning = await deps.guard(access.entry, {
|
|
71
71
|
filter: filter.filter,
|
|
72
|
-
...sort && { sort }
|
|
73
|
-
limit: cardPhotoLimit
|
|
72
|
+
...sort && { sort }
|
|
74
73
|
}, {
|
|
75
74
|
model: access.model,
|
|
76
75
|
scope
|
package/dist/list-service.js
CHANGED
|
@@ -9,8 +9,9 @@ import { readTransaction } from "./transactions.js";
|
|
|
9
9
|
import { deletedRows, listParams, pageSizeOf, parseParams, seekParams, selectedFields } from "./params.js";
|
|
10
10
|
import { labelRequests, relatedLabels } from "./related-labels.js";
|
|
11
11
|
import { photosRequest, recordPhotos } from "./list-photos.js";
|
|
12
|
-
import {
|
|
12
|
+
import { countUpTo, estimateCount, listQuery, seekToPosition } from "@protobase/query";
|
|
13
13
|
//#region src/list-service.ts
|
|
14
|
+
const exactCountLimit = 1e4;
|
|
14
15
|
/**
|
|
15
16
|
* AIP-132 list, the `:search` variant and (with `jump`) `:seek`: both take the same parameters, from the query string or a JSON body.
|
|
16
17
|
* Everything the caller names (filter, order_by, fields, search) is checked against the model as this caller sees it,
|
|
@@ -82,7 +83,7 @@ const listRecords = async (deps, entry, session, input, maxFilterLength, jump =
|
|
|
82
83
|
etags: await Promise.all(rows.map((row, i) => etagOf(entry, page.rows[i], row))),
|
|
83
84
|
permissions: deps.rowPermissions(entry.name) ? await rowPermissions(trx, access, rows) : void 0,
|
|
84
85
|
estimate: await estimateCount(trx, queryModel, filter, scope),
|
|
85
|
-
...params.count === "exact" && {
|
|
86
|
+
...params.count === "exact" && { counted: await countUpTo(trx, queryModel, 10001, filter, scope) },
|
|
86
87
|
...params.labels !== void 0 && { labels: await relatedLabels(trx, labels, rows) }
|
|
87
88
|
};
|
|
88
89
|
});
|
|
@@ -99,7 +100,7 @@ const listRecords = async (deps, entry, session, input, maxFilterLength, jump =
|
|
|
99
100
|
next_page_token: result.page.nextCursor ?? "",
|
|
100
101
|
...position !== void 0 && { prev_page_token: result.page.prevCursor ?? "" },
|
|
101
102
|
total_size_estimate: result.estimate,
|
|
102
|
-
...result.
|
|
103
|
+
...result.counted !== void 0 && result.counted <= exactCountLimit && { total_size: result.counted },
|
|
103
104
|
...result.labels && { labels: result.labels },
|
|
104
105
|
...included && { photos: included.photos }
|
|
105
106
|
}
|
package/dist/map-service.js
CHANGED
|
@@ -48,7 +48,7 @@ const mapRecords = async (deps, entry, session, input) => {
|
|
|
48
48
|
body: {
|
|
49
49
|
items: body.items,
|
|
50
50
|
buckets: [],
|
|
51
|
-
total_size: body.total_size ?? found,
|
|
51
|
+
total_size: body.total_size ?? (found > 200 ? body.total_size_estimate : found),
|
|
52
52
|
...body.labels && { labels: body.labels },
|
|
53
53
|
...body.photos && { photos: body.photos }
|
|
54
54
|
}
|
|
@@ -93,7 +93,7 @@ const parameters = {
|
|
|
93
93
|
count: {
|
|
94
94
|
name: "count",
|
|
95
95
|
in: "query",
|
|
96
|
-
description: "`exact` adds `total_size`, a
|
|
96
|
+
description: "`exact` adds `total_size`, a count of the matches when at most 10,000 match",
|
|
97
97
|
schema: {
|
|
98
98
|
type: "string",
|
|
99
99
|
enum: ["exact"]
|
|
@@ -21,7 +21,10 @@ const collectionRoutes = (deps) => {
|
|
|
21
21
|
const { body, warning } = await listRecords(deps, entry, session, query);
|
|
22
22
|
return c.json(body, 200, warningHeader(warning));
|
|
23
23
|
}
|
|
24
|
-
if (action === "facets")
|
|
24
|
+
if (action === "facets") {
|
|
25
|
+
const { body, warning } = await facets(deps, entry, session, query);
|
|
26
|
+
return c.json(body, 200, warningHeader(warning));
|
|
27
|
+
}
|
|
25
28
|
if (action === "series") return c.json(await timeSeries(deps, entry, session, query));
|
|
26
29
|
if (action === "histogram") return c.json(await numericHistogram(deps, entry, session, query));
|
|
27
30
|
if (action === "map") {
|
package/dist/scan-guard.js
CHANGED
|
@@ -1,70 +1,43 @@
|
|
|
1
1
|
import { HttpProblem } from "./problem.js";
|
|
2
|
-
import {
|
|
3
|
-
import { filterFields, filterShape } from "./filter-shape.js";
|
|
4
|
-
import { buildListQuery, compileFilter, explainGuard } from "@protobase/query";
|
|
5
|
-
import { sql } from "kysely";
|
|
2
|
+
import { tableIndexes, uncoveredColumns } from "@protobase/query";
|
|
6
3
|
//#region src/scan-guard.ts
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
];
|
|
15
|
-
let query = db.selectFrom(sql`${tableId(model)}`.as("t")).select(sql.lit(1).as("one"));
|
|
16
|
-
for (const condition of conditions) query = query.where(condition);
|
|
17
|
-
return query;
|
|
4
|
+
const reading = (column) => {
|
|
5
|
+
switch (column.use) {
|
|
6
|
+
case "filter": return `Filtering on "${column.field}"`;
|
|
7
|
+
case "access": return `The filter your role is limited by, on "${column.field}",`;
|
|
8
|
+
case "sort": return `Sorting by "${column.field}"`;
|
|
9
|
+
case "facet": return `Counting the values of "${column.field}"`;
|
|
10
|
+
}
|
|
18
11
|
};
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
return
|
|
12
|
+
const describe = (column) => {
|
|
13
|
+
const kind = column.need === "btree" ? "an index" : column.need === "gin" ? "a GIN index" : "a pg_trgm index";
|
|
14
|
+
return `${reading(column)} needs ${kind} that serves the column "${column.column}". Add one, for example: ${column.suggestion}`;
|
|
22
15
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (columns.length === 0) return "an index that covers the filter your role is limited by";
|
|
28
|
-
return `create index on ${table} (${columns.map((column) => `"${column}"`).join(", ")})`;
|
|
29
|
-
};
|
|
30
|
-
/** Plans each list query shape once (EXPLAIN, never executed) and remembers the verdict. */
|
|
16
|
+
/**
|
|
17
|
+
* Checks that an index serves every column a read filters, sorts or groups by, from the table's indexes in the catalog
|
|
18
|
+
* (read once per table and kept for the process lifetime), so a read is never refused or allowed by how many rows match.
|
|
19
|
+
*/
|
|
31
20
|
const createScanGuard = (db, options = {}) => {
|
|
32
|
-
const { mode = "reject"
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
sortRows: Number.MAX_SAFE_INTEGER
|
|
37
|
-
};
|
|
38
|
-
const judge = async (entry, shape, context) => {
|
|
39
|
-
const filtered = shape.filter ? await explainGuard(db, filteredQuery(db, entry, shape, context), thresholds) : void 0;
|
|
40
|
-
const paged = await explainGuard(db, await pagedQuery(db, shape, context), thresholds);
|
|
41
|
-
const sortField = shape.sort?.[0] && entry.model.fields[shape.sort[0][0]];
|
|
42
|
-
const suggestions = [...filtered && !filtered.ok ? [indexStatement(entry, filterFields(shape.filter))] : [], ...!paged.ok && sortField ? [indexStatement(entry, [sortField])] : []];
|
|
43
|
-
return {
|
|
44
|
-
findings: [...filtered?.findings ?? [], ...paged.findings],
|
|
45
|
-
suggestion: suggestions[0] ?? indexStatement(entry, filterFields(shape.filter))
|
|
46
|
-
};
|
|
47
|
-
};
|
|
48
|
-
const verdictFor = async (entry, shape, context) => {
|
|
49
|
-
const key = `${entry.name}|${filterShape(context.scope.rowFilter)}|${filterShape(shape.filter)}|${(shape.sort ?? []).map(([name, direction]) => `${name}:${direction}`).join(",")}`;
|
|
50
|
-
const known = verdicts.get(key);
|
|
21
|
+
const { mode = "reject" } = options;
|
|
22
|
+
const tables = /* @__PURE__ */ new Map();
|
|
23
|
+
const indexesOf = async (entry) => {
|
|
24
|
+
const known = tables.get(entry.name);
|
|
51
25
|
if (known) return known;
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return verdict;
|
|
26
|
+
const read = await tableIndexes(db, entry.model);
|
|
27
|
+
tables.set(entry.name, read);
|
|
28
|
+
return read;
|
|
56
29
|
};
|
|
57
|
-
/** Throws a 400 problem for
|
|
58
|
-
return async (entry,
|
|
30
|
+
/** Throws a 400 problem for a column no index serves in `reject` mode; in `warn` mode returns the warning text. */
|
|
31
|
+
return async (entry, query, context) => {
|
|
59
32
|
if (mode === "off") return void 0;
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
if (!
|
|
63
|
-
const detail =
|
|
33
|
+
const columns = uncoveredColumns(context.model, await indexesOf(entry), query, context.scope);
|
|
34
|
+
const first = columns[0];
|
|
35
|
+
if (!first) return void 0;
|
|
36
|
+
const detail = describe(first);
|
|
64
37
|
if (mode === "warn") return detail;
|
|
65
38
|
throw new HttpProblem(400, "expensive-query", "Bad Request", detail, {
|
|
66
|
-
|
|
67
|
-
suggestion
|
|
39
|
+
columns,
|
|
40
|
+
suggestion: first.suggestion
|
|
68
41
|
});
|
|
69
42
|
};
|
|
70
43
|
};
|
package/dist/view-access.js
CHANGED
|
@@ -14,6 +14,17 @@ const pruneRelated = (item, resource, models) => {
|
|
|
14
14
|
} : rest;
|
|
15
15
|
return relatedErrors(narrowed, resource, models).length === 0 ? narrowed : void 0;
|
|
16
16
|
};
|
|
17
|
+
const pruneGallery = (item, resource, model, models) => {
|
|
18
|
+
const image = item.image !== void 0 && Object.hasOwn(model.fields, item.image) ? item.image : void 0;
|
|
19
|
+
const photos = item.photos && cardPhotosErrors(item.photos, resource, models).length === 0 ? item.photos : void 0;
|
|
20
|
+
if (image === void 0 && photos === void 0) return void 0;
|
|
21
|
+
return {
|
|
22
|
+
kind: "gallery",
|
|
23
|
+
title: item.title,
|
|
24
|
+
...image !== void 0 && { image },
|
|
25
|
+
...photos && { photos }
|
|
26
|
+
};
|
|
27
|
+
};
|
|
17
28
|
const keepDefault = ({ default: opens, displays, ...list }) => {
|
|
18
29
|
const shown = displays && displays.length > 0 ? displays : void 0;
|
|
19
30
|
const kept = opens === tableDisplay || shown?.some((display) => display.name === opens);
|
|
@@ -97,11 +108,12 @@ const widgetShape = shapes.oneOf({
|
|
|
97
108
|
label: shapes.data
|
|
98
109
|
}, { requires: ["fields"] })
|
|
99
110
|
});
|
|
100
|
-
const layoutShape = (resource, models) => shapes.oneOf({
|
|
111
|
+
const layoutShape = (resource, model, models) => shapes.oneOf({
|
|
101
112
|
section: shapes.object({
|
|
102
113
|
kind: shapes.data,
|
|
103
114
|
title: shapes.data,
|
|
104
115
|
help: shapes.data,
|
|
116
|
+
helpWhen: shapes.data,
|
|
105
117
|
fields: shapes.list(shapes.field, { nonEmpty: true })
|
|
106
118
|
}, { requires: ["fields"] }),
|
|
107
119
|
sidebar: shapes.object({
|
|
@@ -123,7 +135,8 @@ const layoutShape = (resource, models) => shapes.oneOf({
|
|
|
123
135
|
longitude: shapes.field,
|
|
124
136
|
zoom: shapes.data,
|
|
125
137
|
sidebar: shapes.data
|
|
126
|
-
}, { requires: ["latitude", "longitude"] })
|
|
138
|
+
}, { requires: ["latitude", "longitude"] }),
|
|
139
|
+
gallery: shapes.map((item) => pruneGallery(item, resource, model, models))
|
|
127
140
|
});
|
|
128
141
|
const linkShape = (model, models) => shapes.map((run) => {
|
|
129
142
|
return ("to" in run ? addressErrors(run.to, {
|
|
@@ -166,12 +179,14 @@ const viewShape = (resource, model, models, prompted) => shapes.object({
|
|
|
166
179
|
fields: shapes.keys(),
|
|
167
180
|
list: listShape(resource, models, prompted),
|
|
168
181
|
filters: shapes.list(widgetShape),
|
|
169
|
-
layout: shapes.list(layoutShape(resource, models)),
|
|
182
|
+
layout: shapes.list(layoutShape(resource, model, models)),
|
|
170
183
|
chart: shapes.object({
|
|
171
184
|
field: shapes.field,
|
|
172
185
|
range: shapes.data,
|
|
173
186
|
granularity: shapes.data
|
|
174
187
|
}, { requires: ["field"] }),
|
|
188
|
+
recordMode: shapes.data,
|
|
189
|
+
helpWhen: shapes.data,
|
|
175
190
|
saveFeedback: shapes.data,
|
|
176
191
|
actions: shapes.list(actionShape(model, models)),
|
|
177
192
|
listOf: shapes.field,
|
|
@@ -181,7 +196,7 @@ const viewShape = (resource, model, models, prompted) => shapes.object({
|
|
|
181
196
|
});
|
|
182
197
|
/**
|
|
183
198
|
* The view with every reference to a field the caller cannot read removed: list columns, sort, search and displays,
|
|
184
|
-
* field hints, filter widgets, record sections, maps and custom components' props, the chart, title and search result, the
|
|
199
|
+
* field hints, filter widgets, record sections, maps, galleries and custom components' props, the chart, title and search result, the
|
|
185
200
|
* actions that set or read one, and the sidebar's `recent` group. Nothing in it may name a hidden field. Related sections
|
|
186
201
|
* and links are checked against `models`, the caller's view of every resource, and so are `searchesOf`, cards' favorite
|
|
187
202
|
* buttons and the "Save search" button; the buttons stay for a visitor when their resource is one of `prompted`, those
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@better-auth/passkey": "^1.7.7",
|
|
33
|
-
"@protobase/layout": "0.
|
|
34
|
-
"@protobase/query": "0.
|
|
35
|
-
"@protobase/schema": "0.
|
|
33
|
+
"@protobase/layout": "0.19.0",
|
|
34
|
+
"@protobase/query": "0.19.0",
|
|
35
|
+
"@protobase/schema": "0.19.0",
|
|
36
36
|
"@react-email/render": "^2.1.0",
|
|
37
37
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
38
38
|
"@sentry/core": "^11.7.0",
|
package/dist/filter-shape.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
//#region src/filter-shape.ts
|
|
2
|
-
/** The filter with its values blanked out: queries that differ only in values share a shape. */
|
|
3
|
-
const filterShape = (filter) => {
|
|
4
|
-
if (!filter) return "";
|
|
5
|
-
switch (filter.kind) {
|
|
6
|
-
case "and":
|
|
7
|
-
case "or": return `${filter.kind}(${filter.args.map(filterShape).join(",")})`;
|
|
8
|
-
case "not": return `not(${filterShape(filter.arg)})`;
|
|
9
|
-
case "compare": return `${filter.field.name}${filter.op}?`;
|
|
10
|
-
case "search": return "search";
|
|
11
|
-
default: return `${filter.kind}:${filter.field.name}`;
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
const filterFields = (filter) => {
|
|
15
|
-
if (!filter) return [];
|
|
16
|
-
switch (filter.kind) {
|
|
17
|
-
case "and":
|
|
18
|
-
case "or": return filter.args.flatMap(filterFields);
|
|
19
|
-
case "not": return filterFields(filter.arg);
|
|
20
|
-
case "search": return [];
|
|
21
|
-
default: return [filter.field];
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
//#endregion
|
|
25
|
-
export { filterFields, filterShape };
|