tsense 0.1.0 → 0.2.0-next.2

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.
@@ -0,0 +1,93 @@
1
+ const ARRAY_OPERATORS = new Set(["notIn"]);
2
+ function serializeValue(value) {
3
+ if (value instanceof Date) {
4
+ return value.toISOString();
5
+ }
6
+ return String(value);
7
+ }
8
+ function coerceValue(raw, columnType) {
9
+ if (columnType === "number") {
10
+ return Number(raw);
11
+ }
12
+ if (columnType === "boolean") {
13
+ return raw === "true";
14
+ }
15
+ if (columnType === "date") {
16
+ return new Date(raw);
17
+ }
18
+ return raw;
19
+ }
20
+ export function serializeFilter(filter, descriptor) {
21
+ const params = new URLSearchParams();
22
+ const columnMap = new Map(descriptor.columns.map((c) => [c.key, c]));
23
+ for (const [key, value] of Object.entries(filter)) {
24
+ const column = columnMap.get(key);
25
+ if (!column || value == null) {
26
+ continue;
27
+ }
28
+ if (Array.isArray(value)) {
29
+ if (value.length === 1) {
30
+ params.set(key, serializeValue(value[0]));
31
+ }
32
+ else if (value.length > 1) {
33
+ params.set(key, value.map((v) => serializeValue(v)).join(","));
34
+ }
35
+ continue;
36
+ }
37
+ if (typeof value === "object" && !(value instanceof Date)) {
38
+ for (const [op, opValue] of Object.entries(value)) {
39
+ if (opValue == null) {
40
+ continue;
41
+ }
42
+ if (Array.isArray(opValue)) {
43
+ params.set(`${key}.${op}`, opValue.map((v) => serializeValue(v)).join(","));
44
+ }
45
+ else {
46
+ params.set(`${key}.${op}`, serializeValue(opValue));
47
+ }
48
+ }
49
+ continue;
50
+ }
51
+ params.set(key, serializeValue(value));
52
+ }
53
+ return params;
54
+ }
55
+ export function deserializeFilter(params, descriptor) {
56
+ const result = {};
57
+ const columnMap = new Map(descriptor.columns.map((c) => [c.key, c]));
58
+ for (const [paramKey, rawValue] of params.entries()) {
59
+ const dotIndex = paramKey.indexOf(".");
60
+ if (dotIndex !== -1) {
61
+ const field = paramKey.slice(0, dotIndex);
62
+ const operator = paramKey.slice(dotIndex + 1);
63
+ const column = columnMap.get(field);
64
+ if (!column) {
65
+ continue;
66
+ }
67
+ const existing = (result[field] ?? {});
68
+ if (ARRAY_OPERATORS.has(operator)) {
69
+ existing[operator] = rawValue
70
+ .split(",")
71
+ .map((v) => coerceValue(v, column.type));
72
+ }
73
+ else {
74
+ existing[operator] = coerceValue(rawValue, column.type);
75
+ }
76
+ result[field] = existing;
77
+ continue;
78
+ }
79
+ const column = columnMap.get(paramKey);
80
+ if (!column) {
81
+ continue;
82
+ }
83
+ if (rawValue.includes(",")) {
84
+ result[paramKey] = rawValue
85
+ .split(",")
86
+ .map((v) => coerceValue(v, column.type));
87
+ }
88
+ else {
89
+ result[paramKey] = coerceValue(rawValue, column.type);
90
+ }
91
+ }
92
+ return result;
93
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type { TsenseFieldMeta, TsenseFieldType } from "./env.js";
2
+ export { rank } from "./rank.js";
2
3
  export { DateTransformer } from "./transformers/date.js";
3
4
  export { defaultTransformers } from "./transformers/defaults.js";
4
5
  export type { FieldTransformer } from "./transformers/types.js";
5
6
  export { TSense } from "./tsense.js";
6
- export type { ConnectionConfig, DeleteResult, FilterFor, HighlightOptions, ProjectSearch, SearchListOptions, SearchListResult, SearchOptions, SearchOptionsPlain, SearchOptionsWithOmit, SearchOptionsWithPick, SearchResult, SyncConfig, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult, } from "./types.js";
7
+ export type { ConnectionConfig, DeleteResult, FilterFor, HighlightOptions, NumberFilter, ProjectSearch, SearchListOptions, SearchListResult, SearchInput, SearchOptions, ScopedCollection, SearchOptionsPlain, SearchOptionsWithOmit, SearchOptionsWithPick, SearchResult, StringFilter, SyncConfig, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult, WithNull, } from "./types.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { rank } from "./rank.js";
1
2
  export { DateTransformer } from "./transformers/date.js";
2
3
  export { defaultTransformers } from "./transformers/defaults.js";
3
4
  export { TSense } from "./tsense.js";
@@ -5,7 +5,7 @@ export declare class TSenseMigrator {
5
5
  private localFields;
6
6
  private defaultSortingField;
7
7
  private axios;
8
- constructor(collectionName: string, localFields: FieldSchema[], defaultSortingField: string | undefined, axios: AxiosInstance);
8
+ constructor(collectionName: string, localFields: readonly FieldSchema[], defaultSortingField: string | undefined, axios: AxiosInstance);
9
9
  sync(): Promise<void>;
10
10
  private exists;
11
11
  private getRemoteFields;
package/dist/migrator.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const COMPARABLE_KEYS = ["type", "facet", "sort", "index", "optional"];
2
- const NESTED_TYPES = ["object", "object[]"];
2
+ const NESTED_TYPES = new Set(["object", "object[]"]);
3
3
  export class TSenseMigrator {
4
4
  collectionName;
5
5
  localFields;
@@ -102,7 +102,7 @@ export class TSenseMigrator {
102
102
  .catch(() => false);
103
103
  }
104
104
  async create() {
105
- const enable_nested_fields = this.localFields.some((f) => NESTED_TYPES.includes(f.type));
105
+ const enable_nested_fields = this.localFields.some((f) => NESTED_TYPES.has(f.type));
106
106
  await this.axios({
107
107
  method: "POST",
108
108
  url: "/collections",
package/dist/rank.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export declare function rank<T>({ db, id_key, ts }: {
2
+ db: T[];
3
+ id_key: keyof T;
4
+ ts: {
5
+ id: string;
6
+ }[];
7
+ }): T[];
package/dist/rank.js ADDED
@@ -0,0 +1,12 @@
1
+ export function rank({ db, id_key, ts, }) {
2
+ const mapped = new Map(ts.map((t, i) => [t.id, i]));
3
+ const result = [];
4
+ for (const item of db) {
5
+ const position = mapped.get(String(item[id_key]));
6
+ if (position == null) {
7
+ continue;
8
+ }
9
+ result[position] = item;
10
+ }
11
+ return result.filter(Boolean);
12
+ }
@@ -0,0 +1,60 @@
1
+ import { type ReactNode } from "react";
2
+ import type { FilterValue } from "../filters/filter-state.js";
3
+ import type { FilterDescriptor } from "../filters/index.js";
4
+ import type { FilterFor } from "../types.js";
5
+ export type FieldSelectSlotProps = {
6
+ columns: FilterDescriptor["columns"];
7
+ value: string | undefined;
8
+ onChange: (field: string) => void;
9
+ };
10
+ export type ConditionSelectSlotProps = {
11
+ conditions: FilterDescriptor["columns"][number]["conditions"];
12
+ value: string | undefined;
13
+ onChange: (condition: string) => void;
14
+ disabled: boolean;
15
+ };
16
+ export type ValueInputSlotProps = {
17
+ column: FilterDescriptor["columns"][number];
18
+ condition: string;
19
+ value: FilterValue;
20
+ onChange: (value: FilterValue) => void;
21
+ };
22
+ export type RemoveButtonSlotProps = {
23
+ onClick: () => void;
24
+ };
25
+ export type AddButtonSlotProps = {
26
+ onClick: () => void;
27
+ };
28
+ export type PresetButtonSlotProps = {
29
+ preset: {
30
+ field: string;
31
+ name: string;
32
+ };
33
+ onClick: () => void;
34
+ };
35
+ export type RowSlotProps = {
36
+ index: number;
37
+ fieldSelect: ReactNode;
38
+ conditionSelect: ReactNode;
39
+ valueInput: ReactNode | null;
40
+ removeButton: ReactNode;
41
+ };
42
+ export type RootSlotProps = {
43
+ rows: ReactNode;
44
+ addButton: ReactNode;
45
+ presets: ReactNode | null;
46
+ };
47
+ type FilterBuilderProps<T> = {
48
+ descriptor: FilterDescriptor<T>;
49
+ onChange?: (filter: FilterFor<T>) => void;
50
+ renderRoot?: (props: RootSlotProps) => ReactNode;
51
+ renderRow?: (props: RowSlotProps) => ReactNode;
52
+ renderFieldSelect?: (props: FieldSelectSlotProps) => ReactNode;
53
+ renderConditionSelect?: (props: ConditionSelectSlotProps) => ReactNode;
54
+ renderValueInput?: (props: ValueInputSlotProps) => ReactNode;
55
+ renderAddButton?: (props: AddButtonSlotProps) => ReactNode;
56
+ renderRemoveButton?: (props: RemoveButtonSlotProps) => ReactNode;
57
+ renderPresetButton?: (props: PresetButtonSlotProps) => ReactNode;
58
+ };
59
+ export declare function FilterBuilder<T>({ descriptor, onChange, renderRoot, renderRow, renderFieldSelect, renderConditionSelect, renderValueInput, renderAddButton, renderRemoveButton, renderPresetButton }: FilterBuilderProps<T>): ReactNode;
60
+ export {};
@@ -0,0 +1,114 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment, useEffect, useRef } from "react";
3
+ import { useFilterBuilder } from "./use-filter-builder.js";
4
+ // ======== Helpers ========
5
+ function formatDateForInput(date) {
6
+ if (!(date instanceof Date))
7
+ return "";
8
+ return date.toISOString().slice(0, 10);
9
+ }
10
+ function parseDateInput(str) {
11
+ if (!str)
12
+ return undefined;
13
+ return new Date(str + "T00:00:00Z");
14
+ }
15
+ function asTuple(value) {
16
+ if (Array.isArray(value)) {
17
+ return [value[0], value[1]];
18
+ }
19
+ return [undefined, undefined];
20
+ }
21
+ function renderSelectOptions(options, defaultValue) {
22
+ const opts = defaultValue
23
+ ? [{ key: "", label: defaultValue }, ...options]
24
+ : options;
25
+ return opts.map((o) => (_jsx("option", { value: o.key, children: o.label }, o.key)));
26
+ }
27
+ function defaultFieldSelect({ columns, value, onChange, }) {
28
+ return (_jsx("select", { className: "rounded border px-2 py-1", value: value ?? "", onChange: (e) => onChange(e.target.value), children: renderSelectOptions(columns, "Column") }));
29
+ }
30
+ function defaultConditionSelect({ conditions, value, onChange, disabled, }) {
31
+ return (_jsx("select", { className: "rounded border px-2 py-1", value: value ?? "", onChange: (e) => onChange(e.target.value), disabled: disabled, children: renderSelectOptions(conditions, "Condition") }));
32
+ }
33
+ function defaultValueInput({ column, condition, value, onChange, }) {
34
+ if (condition === "between" && column.type === "date") {
35
+ const tuple = asTuple(value);
36
+ return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { className: "rounded border px-2 py-1", type: "date", value: formatDateForInput(tuple[0]), onChange: (e) => onChange([parseDateInput(e.target.value), tuple[1]]) }), _jsx("span", { className: "text-sm text-gray-500", children: "and" }), _jsx("input", { className: "rounded border px-2 py-1", type: "date", value: formatDateForInput(tuple[1]), onChange: (e) => onChange([tuple[0], parseDateInput(e.target.value)]) })] }));
37
+ }
38
+ if (condition === "between") {
39
+ const tuple = asTuple(value);
40
+ return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { className: "w-24 rounded border px-2 py-1", type: "number", value: String(tuple[0] ?? ""), onChange: (e) => onChange([Number(e.target.value), tuple[1]]) }), _jsx("span", { className: "text-sm text-gray-500", children: "and" }), _jsx("input", { className: "w-24 rounded border px-2 py-1", type: "number", value: String(tuple[1] ?? ""), onChange: (e) => onChange([tuple[0], Number(e.target.value)]) })] }));
41
+ }
42
+ if (column.values) {
43
+ const selected = Array.isArray(value) ? value : [];
44
+ return (_jsx("div", { className: "flex flex-wrap gap-2", children: column.values.map((v) => {
45
+ const checked = selected.includes(v.value);
46
+ return (_jsxs("label", { className: "flex cursor-pointer select-none items-center gap-1 text-sm", children: [_jsx("input", { type: "checkbox", checked: checked, onChange: () => onChange(checked
47
+ ? selected.filter((s) => s !== v.value)
48
+ : [...selected, v.value]) }), v.label] }, v.value));
49
+ }) }));
50
+ }
51
+ if (column.type === "date") {
52
+ return (_jsx("input", { className: "rounded border px-2 py-1", type: "date", value: formatDateForInput(value), onChange: (e) => onChange(parseDateInput(e.target.value)) }));
53
+ }
54
+ if (column.type === "number") {
55
+ return (_jsx("input", { className: "rounded border px-2 py-1", type: "number", value: String(value ?? ""), onChange: (e) => onChange(Number(e.target.value)) }));
56
+ }
57
+ if (column.type === "boolean") {
58
+ const options = [
59
+ { key: "", label: "Value" },
60
+ { key: "true", label: "true" },
61
+ { key: "false", label: "false" },
62
+ ];
63
+ return (_jsx("select", { className: "rounded border px-2 py-1", value: value == null ? "" : String(value), onChange: (e) => onChange(e.target.value === "true"), children: renderSelectOptions(options) }));
64
+ }
65
+ return (_jsx("input", { className: "rounded border px-2 py-1", type: "text", value: typeof value === "string" ? value : "", onChange: (e) => onChange(e.target.value) }));
66
+ }
67
+ function defaultRemoveButton({ onClick }) {
68
+ return (_jsx("button", { className: "text-red-500 hover:text-red-700", onClick: onClick, children: "\u00D7" }));
69
+ }
70
+ function defaultAddButton({ onClick }) {
71
+ return (_jsx("button", { className: "text-blue-500 hover:text-blue-700", onClick: onClick, children: "+ Add filter" }));
72
+ }
73
+ function defaultPresetButton({ preset, onClick }) {
74
+ return (_jsx("button", { className: "rounded bg-gray-100 px-2 py-1 text-sm hover:bg-gray-200", onClick: onClick, children: preset.name }));
75
+ }
76
+ export function FilterBuilder({ descriptor, onChange, renderRoot, renderRow, renderFieldSelect, renderConditionSelect, renderValueInput, renderAddButton, renderRemoveButton, renderPresetButton, }) {
77
+ const filters = useFilterBuilder(descriptor);
78
+ const onChangeRef = useRef(onChange);
79
+ onChangeRef.current = onChange;
80
+ useEffect(() => {
81
+ onChangeRef.current?.(filters.result);
82
+ }, [filters.result]);
83
+ const FieldSelect = renderFieldSelect ?? defaultFieldSelect;
84
+ const ConditionSelect = renderConditionSelect ?? defaultConditionSelect;
85
+ const ValueInput = renderValueInput ?? defaultValueInput;
86
+ const RemoveButton = renderRemoveButton ?? defaultRemoveButton;
87
+ const AddButton = renderAddButton ?? defaultAddButton;
88
+ const PresetButton = renderPresetButton ?? defaultPresetButton;
89
+ const rows = filters.rows.map((row, i) => {
90
+ const fieldSelect = (_jsx(FieldSelect, { columns: filters.columns, value: row.field, onChange: (field) => filters.setField(i, field) }));
91
+ const conditionSelect = (_jsx(ConditionSelect, { conditions: row.field ? filters.conditionsFor(row.field) : [], value: row.condition, onChange: (condition) => filters.setCondition(i, condition), disabled: !row.field }));
92
+ const column = row.field
93
+ ? filters.columns.find((c) => c.key === row.field)
94
+ : undefined;
95
+ const valueInput = row.field && row.condition && column ? (_jsx(ValueInput, { column: column, condition: row.condition, value: row.value, onChange: (v) => filters.setValue(i, v) })) : null;
96
+ const removeButton = _jsx(RemoveButton, { onClick: () => filters.remove(i) });
97
+ if (renderRow) {
98
+ return (_jsx(Fragment, { children: renderRow({
99
+ index: i,
100
+ fieldSelect,
101
+ conditionSelect,
102
+ valueInput,
103
+ removeButton,
104
+ }) }, row.id));
105
+ }
106
+ return (_jsxs("div", { className: "flex items-center gap-2", children: [fieldSelect, conditionSelect, valueInput, removeButton] }, row.id));
107
+ });
108
+ const addButton = _jsx(AddButton, { onClick: filters.add });
109
+ const presets = filters.presets.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-1 border-t pt-2", children: filters.presets.map((preset) => (_jsx(Fragment, { children: _jsx(PresetButton, { preset: preset, onClick: () => filters.applyPreset(preset.field, preset.name) }) }, `${preset.field}-${preset.name}`))) })) : null;
110
+ if (renderRoot) {
111
+ return _jsx(_Fragment, { children: renderRoot({ rows: _jsx(_Fragment, { children: rows }), addButton, presets }) });
112
+ }
113
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [rows, addButton, presets] }));
114
+ }
@@ -0,0 +1,4 @@
1
+ export { FilterBuilder } from "./filter-builder.js";
2
+ export type { AddButtonSlotProps, ConditionSelectSlotProps, FieldSelectSlotProps, PresetButtonSlotProps, RemoveButtonSlotProps, RootSlotProps, RowSlotProps, ValueInputSlotProps, } from "./filter-builder.js";
3
+ export type { FilterValue } from "../filters/filter-state.js";
4
+ export { useFilterBuilder } from "./use-filter-builder.js";
@@ -0,0 +1,2 @@
1
+ export { FilterBuilder } from "./filter-builder.js";
2
+ export { useFilterBuilder } from "./use-filter-builder.js";
@@ -0,0 +1,24 @@
1
+ import { type FilterRow, type FilterValue } from "../filters/filter-state.js";
2
+ import type { FilterDescriptor } from "../filters/index.js";
3
+ type Preset = {
4
+ field: string;
5
+ name: string;
6
+ };
7
+ type UseFilterBuilderReturn = {
8
+ columns: FilterDescriptor["columns"];
9
+ rows: FilterRow[];
10
+ add: () => void;
11
+ addWithField: (field: string) => void;
12
+ remove: (index: number) => void;
13
+ setField: (index: number, field: string) => void;
14
+ setCondition: (index: number, condition: string) => void;
15
+ setValue: (index: number, value: FilterValue) => void;
16
+ clear: () => void;
17
+ conditionsFor: (field: string) => FilterDescriptor["columns"][number]["conditions"];
18
+ columnFor: (field: string) => FilterDescriptor["columns"][number];
19
+ presets: Preset[];
20
+ applyPreset: (field: string, name: string) => void;
21
+ result: Record<string, unknown>;
22
+ };
23
+ export declare function useFilterBuilder<T>(descriptor: FilterDescriptor<T>): UseFilterBuilderReturn;
24
+ export {};
@@ -0,0 +1,23 @@
1
+ import { useMemo, useState } from "react";
2
+ import { addRow, addRowWithField, applyPreset as applyPresetState, buildResult, clearState, columnFor, conditionsFor, createInitialState, removeRow, setRowCondition, setRowField, setRowValue, } from "../filters/filter-state.js";
3
+ export function useFilterBuilder(descriptor) {
4
+ const [state, setState] = useState(createInitialState);
5
+ const presets = useMemo(() => descriptor.columns.flatMap((col) => (col.presets ?? []).map((p) => ({ field: col.key, name: p.name }))), [descriptor]);
6
+ const result = useMemo(() => buildResult(state), [state]);
7
+ return {
8
+ columns: descriptor.columns,
9
+ rows: state.rows,
10
+ add: () => setState(addRow),
11
+ addWithField: (field) => setState((s) => addRowWithField(s, field)),
12
+ remove: (index) => setState((s) => removeRow(s, index)),
13
+ setField: (index, field) => setState((s) => setRowField(s, index, field)),
14
+ setCondition: (index, condition) => setState((s) => setRowCondition(s, index, condition)),
15
+ setValue: (index, value) => setState((s) => setRowValue(s, index, value)),
16
+ clear: () => setState(clearState),
17
+ conditionsFor: (field) => conditionsFor(descriptor, field),
18
+ columnFor: (field) => columnFor(descriptor, field),
19
+ presets,
20
+ applyPreset: (field, name) => setState((s) => applyPresetState(s, descriptor, field, name)),
21
+ result,
22
+ };
23
+ }
@@ -1,6 +1,14 @@
1
1
  export const DateTransformer = {
2
2
  match: (expr, domain) => expr === "Date" || domain === "Date",
3
3
  storageType: "int64",
4
- serialize: (date) => date.getTime(),
4
+ serialize: (date) => {
5
+ if (typeof date === "number") {
6
+ return date;
7
+ }
8
+ if (typeof date === "string") {
9
+ return new Date(date).getTime();
10
+ }
11
+ return date.getTime();
12
+ },
5
13
  deserialize: (ts) => new Date(ts),
6
14
  };
package/dist/tsense.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Type } from "arktype";
2
2
  import redaxios from "redaxios";
3
- import type { DeleteResult, FieldSchema, FilterFor, ProjectSearch, SearchListOptions, SearchListResult, SearchOptions, SearchOptionsPlain, SearchResult, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult } from "./types.js";
3
+ import type { DeleteResult, FieldSchema, FilterFor, ProjectSearch, ScopedCollection, SearchListOptions, SearchListResult, SearchOptions, SearchOptionsPlain, SearchResult, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult, WithNull } from "./types.js";
4
4
  declare const redaxiosInstance: {
5
5
  <T>(urlOrConfig: string | redaxios.Options, config?: redaxios.Options | undefined, _method?: any, data?: any, _undefined?: undefined): Promise<redaxios.Response<T>>;
6
6
  request: (<T_1 = any>(config?: redaxios.Options | undefined) => Promise<redaxios.Response<T_1>>) | (<T_2 = any>(url: string, config?: redaxios.Options | undefined) => Promise<redaxios.Response<T_2>>);
@@ -53,19 +53,18 @@ declare const redaxiosInstance: {
53
53
  spread<Args, R>(fn: (...args: Args[]) => R): (array: Args[]) => R;
54
54
  CancelToken: AbortController;
55
55
  defaults: redaxios.Options;
56
- create: any;
56
+ create: /*elided*/ any;
57
57
  };
58
58
  };
59
59
  export type AxiosInstance = ReturnType<typeof redaxiosInstance.create>;
60
60
  export declare class TSense<T extends Type> {
61
61
  private options;
62
- fields: FieldSchema[];
62
+ readonly fields: readonly FieldSchema[];
63
63
  private axios;
64
64
  private synced;
65
65
  private fieldTransformers;
66
66
  private dataSyncConfig?;
67
67
  infer: T["infer"];
68
- getFields(): FieldSchema[];
69
68
  constructor(options: TsenseOptions<T>);
70
69
  private getBaseType;
71
70
  private inferType;
@@ -77,6 +76,7 @@ export declare class TSense<T extends Type> {
77
76
  syncSchema(): Promise<void>;
78
77
  private buildObjectFilter;
79
78
  private buildFilter;
79
+ private validateFields;
80
80
  private buildSort;
81
81
  create(): Promise<this>;
82
82
  drop(): Promise<void>;
@@ -88,9 +88,10 @@ export declare class TSense<T extends Type> {
88
88
  search<const O extends SearchOptions<T["infer"]> = SearchOptionsPlain<T["infer"]>>(options: O): Promise<SearchResult<ProjectSearch<T["infer"], O>>>;
89
89
  searchList(options: SearchListOptions<T["infer"]>): Promise<SearchListResult<T["infer"]>>;
90
90
  count(filter?: FilterFor<T["infer"]>): Promise<number>;
91
- upsert(docs: T["infer"] | T["infer"][]): Promise<UpsertResult[]>;
91
+ upsert(docs: WithNull<T["infer"]> | WithNull<T["infer"]>[]): Promise<UpsertResult[]>;
92
92
  syncData(options?: SyncOptions): Promise<SyncResult>;
93
93
  private purgeOrphans;
94
94
  exportIds(): Promise<string[]>;
95
+ scoped(baseFilter: FilterFor<T["infer"]>): ScopedCollection<T["infer"]>;
95
96
  }
96
97
  export {};