-
+
{sections.map(section => (
{entries.filter(e => e.data.package === section && !e.data.secondary).map(e => (
diff --git a/packages/react/readme.md b/packages/react/readme.md
index 0b533c7b..50e464b3 100644
--- a/packages/react/readme.md
+++ b/packages/react/readme.md
@@ -11,13 +11,13 @@
```tsx
const Component = () => {
- const userTable = useTable((model: Model) => model.users);
- const { data: firstUser } = await useMany(userTable, {
+ const { data: users } = useMany(userTable, {
where: {
name: { in: ["Alice", "Charlie"] },
age: { gt: 24 },
},
});
+
...
}
```
diff --git a/packages/react/src/retrieval/useFirst.ts b/packages/react/src/retrieval/useFirst.ts
index e54bd0c6..4bc81b17 100644
--- a/packages/react/src/retrieval/useFirst.ts
+++ b/packages/react/src/retrieval/useFirst.ts
@@ -7,7 +7,7 @@ import { useMany } from "./useMany";
*
* @example
* // Retrieve the first user
- * const { data: firstUser } = await useFirst(userTable);
+ * const { data: firstUser } = useFirst(userTable);
*/
export function useFirst, P extends PrimaryKeyOf>(
table: Table
@@ -18,7 +18,7 @@ export function useFirst, P extends PrimaryKeyOf>(
*
* @example
* // Retrieve the first user named 'Alice'
- * const { data: firstUser } = await useFirst(userTable, {
+ * const { data: firstUser } = useFirst(userTable, {
* where: {
* name: "Alice"
* }
@@ -34,7 +34,7 @@ export function useFirst, P extends PrimaryKeyOf>(
*
* @example
* // Retrieve the 'Alice' user by their id
- * const { data: firstUser } = await useFirst(userTable, 'alice-uuid');
+ * const { data: firstUser } = useFirst(userTable, 'alice-uuid');
*/
export function useFirst, P extends PrimaryKeyOf>(
table: Table,
@@ -47,16 +47,19 @@ export function useFirst, P extends PrimaryKeyOf>(
): QueryResult {
const primaryKeyProperty = key(table);
let result: QueryResult;
- if(queryOrId === undefined) {
+ if (queryOrId === undefined) {
result = useMany(table);
} else {
- const query = typeof queryOrId === "object" ? queryOrId : { where: { [primaryKeyProperty]: queryOrId } } as unknown as Query;
+ const query =
+ typeof queryOrId === "object"
+ ? queryOrId
+ : ({ where: { [primaryKeyProperty]: queryOrId } } as unknown as Query);
result = useMany(table, query);
}
return {
...result,
- data: result.data ? (result.data[0] ?? null) : undefined,
- error: undefined
+ data: result.data ? result.data[0] ?? null : undefined,
+ error: undefined,
} as QueryResult;
-}
\ No newline at end of file
+}
diff --git a/packages/react/src/retrieval/useOne.ts b/packages/react/src/retrieval/useOne.ts
index 4a927854..43662ca1 100644
--- a/packages/react/src/retrieval/useOne.ts
+++ b/packages/react/src/retrieval/useOne.ts
@@ -1,4 +1,12 @@
-import { Entity, ItemNotFoundError, key, MoreThanOneItemFoundError, PrimaryKeyOf, Query, Table } from "blinkdb";
+import {
+ Entity,
+ ItemNotFoundError,
+ key,
+ MoreThanOneItemFoundError,
+ PrimaryKeyOf,
+ Query,
+ Table,
+} from "blinkdb";
import { QueryResult } from "./types";
import { useMany } from "./useMany";
@@ -9,7 +17,7 @@ import { useMany } from "./useMany";
*
* @example
* // Retrieve the only user named 'Alice'
- * const { data: aliceUser } = await useOne(userTable, {
+ * const { data: aliceUser } = useOne(userTable, {
* where: {
* name: "Alice"
* }
@@ -27,7 +35,7 @@ export function useOne, P extends PrimaryKeyOf>(
*
* @example
* // Retrieve the 'Alice' user by their id
- * const { data: aliceUser } = await useOne(userTable, 'alice-uuid');
+ * const { data: aliceUser } = useOne(userTable, 'alice-uuid');
*/
export function useOne, P extends PrimaryKeyOf>(
table: Table,
@@ -40,31 +48,34 @@ export function useOne, P extends PrimaryKeyOf>(
): QueryResult {
const primaryKeyProperty = key(table);
let result: QueryResult;
- if(queryOrId === undefined) {
+ if (queryOrId === undefined) {
result = useMany(table);
} else {
- const query = typeof queryOrId === "object" ? queryOrId : { where: { [primaryKeyProperty]: queryOrId } } as unknown as Query;
+ const query =
+ typeof queryOrId === "object"
+ ? queryOrId
+ : ({ where: { [primaryKeyProperty]: queryOrId } } as unknown as Query);
result = useMany(table, query);
}
- if(result.state === "done") {
+ if (result.state === "done") {
if (result.data.length === 0) {
return {
state: "error",
data: undefined,
- error: new ItemNotFoundError(queryOrId)
- }
+ error: new ItemNotFoundError(queryOrId),
+ };
} else if (result.data.length > 1) {
return {
state: "error",
data: undefined,
- error: new MoreThanOneItemFoundError(queryOrId)
- }
+ error: new MoreThanOneItemFoundError(queryOrId),
+ };
}
}
return {
...result,
- data: result.data ? result.data[0] : undefined
+ data: result.data ? result.data[0] : undefined,
} as QueryResult;
-}
\ No newline at end of file
+}
diff --git a/packages/vue/jest.config.ts b/packages/vue/jest.config.ts
new file mode 100644
index 00000000..ecd77328
--- /dev/null
+++ b/packages/vue/jest.config.ts
@@ -0,0 +1,14 @@
+import type { Config } from "jest";
+
+const config: Config = {
+ testEnvironment: "jsdom",
+ testEnvironmentOptions: {
+ customExportConditions: ["node", "node-addons"],
+ },
+ transformIgnorePatterns: [],
+ transform: {
+ "^.+\\.(t|j)sx?$": "@swc/jest",
+ },
+};
+
+export default config;
diff --git a/packages/vue/package.json b/packages/vue/package.json
new file mode 100644
index 00000000..f3b3631a
--- /dev/null
+++ b/packages/vue/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@blinkdb/vue",
+ "version": "0.0.1",
+ "description": "Auxiliary package for BlinkDB & Vue",
+ "main": "dist/index.js",
+ "author": "",
+ "license": "MIT",
+ "scripts": {
+ "build": "tsc --outDir ./dist",
+ "test": "jest"
+ },
+ "peerDependencies": {
+ "blinkdb": "^0.13.0",
+ "vue": "^3.0.0"
+ },
+ "devDependencies": {
+ "@testing-library/vue": "^7.0.0"
+ }
+}
diff --git a/packages/vue/readme.md b/packages/vue/readme.md
new file mode 100644
index 00000000..7c9151f5
--- /dev/null
+++ b/packages/vue/readme.md
@@ -0,0 +1,35 @@
+
+
+
+
+
+ BlinkDB is a in-memory JS database optimized for large scale storage
+ on the frontend.
+
+
+
+
+```vue
+
+
+...
+```
+
+# `@blinkdb/vue`
+
+This package contains auxiliary methods for smoothly integrating BlinkDB into [Vue](https://vuejs.org/).
+
+## Getting started
+
+- Read the docs at https://blinkdb.io/docs/vue.
+- Check out the API reference at https://blinkdb.io/docs/reference/vue.
diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts
new file mode 100644
index 00000000..7a543096
--- /dev/null
+++ b/packages/vue/src/index.ts
@@ -0,0 +1,4 @@
+export * from "./types";
+export * from "./watchFirst";
+export * from "./watchMany";
+export * from "./watchOne";
diff --git a/packages/vue/src/testutils.ts b/packages/vue/src/testutils.ts
new file mode 100644
index 00000000..4fd048e3
--- /dev/null
+++ b/packages/vue/src/testutils.ts
@@ -0,0 +1,26 @@
+import { createApp } from "vue";
+
+export interface User {
+ id: string;
+ name: string;
+}
+
+export interface Post {
+ id: string;
+ name: string;
+}
+
+export function withSetup(composable: () => T) {
+ let result: T;
+ const app = createApp({
+ setup() {
+ result = composable();
+ // suppress missing template warning
+ return () => {};
+ },
+ });
+ app.mount(document.createElement("div"));
+ // return the result and the app instance
+ // for testing provide / unmount
+ return [result!, app] as const;
+}
diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts
new file mode 100644
index 00000000..e1076292
--- /dev/null
+++ b/packages/vue/src/types.ts
@@ -0,0 +1,22 @@
+/**
+ * Result returned from a call to `watchMany()`.
+ */
+export type QueryResult = LoadingQueryResult | DoneQueryResult | ErrorQueryResult;
+
+interface LoadingQueryResult {
+ data: undefined;
+ error: undefined;
+ state: "loading";
+}
+
+interface DoneQueryResult {
+ data: T;
+ error: undefined;
+ state: "done";
+}
+
+interface ErrorQueryResult {
+ data: undefined;
+ error: Error;
+ state: "error";
+}
diff --git a/packages/vue/src/watchFirst.spec.ts b/packages/vue/src/watchFirst.spec.ts
new file mode 100644
index 00000000..8d158236
--- /dev/null
+++ b/packages/vue/src/watchFirst.spec.ts
@@ -0,0 +1,196 @@
+import { waitFor } from "@testing-library/vue";
+import { clear, createDB, createTable, insert, insertMany, Table, update } from "blinkdb";
+import { User, withSetup } from "./testutils";
+import { watchFirst } from "./watchFirst";
+
+let userTable: Table;
+
+const users: User[] = [
+ { id: "0", name: "Alice" },
+ { id: "1", name: "Bob" },
+ { id: "2", name: "Charlie" },
+];
+
+beforeEach(() => {
+ const db = createDB();
+ userTable = createTable(db, "users")();
+});
+
+test("shows loading state on first render", async () => {
+ const [{ state, data }, app] = withSetup(() => watchFirst(userTable));
+
+ expect(state.value).toBe("loading");
+ expect(data.value).toBe(undefined);
+
+ app.unmount();
+});
+
+test("shows done state on subsequent renders", async () => {
+ const [{ state, data }, app] = withSetup(() => watchFirst(userTable));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual(null);
+
+ app.unmount();
+});
+
+describe("without filter", () => {
+ beforeEach(async () => {
+ await insertMany(userTable, users);
+ });
+
+ it("returns first user", async () => {
+ const [{ state, data }, app] = withSetup(() => watchFirst(userTable));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual(users[0]);
+
+ app.unmount();
+ });
+
+ it("returns undefined if no users in table", async () => {
+ await clear(userTable);
+ const [{ state, data }, app] = withSetup(() => watchFirst(userTable));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toBe(null);
+
+ app.unmount();
+ });
+
+ it("updates on changes", async () => {
+ const [{ state, data }, app] = withSetup(() => watchFirst(userTable));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ const newUser: User = { id: "", name: "Delta" };
+ await insert(userTable, newUser);
+
+ await waitFor(() => {
+ expect(data.value).toStrictEqual(newUser);
+ });
+
+ app.unmount();
+ });
+});
+
+describe("with filter", () => {
+ beforeEach(async () => {
+ await insertMany(userTable, users);
+ });
+
+ it("returns first user matching filter", async () => {
+ const [{ state, data }, app] = withSetup(() =>
+ watchFirst(userTable, { where: { name: "Bob" } })
+ );
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual(users[1]);
+
+ app.unmount();
+ });
+
+ it("returns undefined if no person matches filter", async () => {
+ const [{ state, data }, app] = withSetup(() =>
+ watchFirst(userTable, { where: { name: "Bobby" } })
+ );
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toBe(null);
+
+ app.unmount();
+ });
+
+ it("doesn't update on changes not matching the query", async () => {
+ const [{ state, data }, app] = withSetup(() =>
+ watchFirst(userTable, { where: { name: "Bob" } })
+ );
+
+ const newUser: User = { id: "", name: "Delta" };
+ await insert(userTable, newUser);
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual(users[1]);
+
+ app.unmount();
+ });
+
+ it("updates on changes matching the query", async () => {
+ const [{ state, data }, app] = withSetup(() =>
+ watchFirst(userTable, { where: { name: "Bob" } })
+ );
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ const newUser: User = { id: "", name: "Bob" };
+ await insert(userTable, newUser);
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual(newUser);
+
+ app.unmount();
+ });
+});
+
+describe("with id", () => {
+ beforeEach(async () => {
+ await insertMany(userTable, users);
+ });
+
+ it("returns first user with the given id", async () => {
+ const [{ state, data }, app] = withSetup(() => watchFirst(userTable, "0"));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual(users[0]);
+
+ app.unmount();
+ });
+
+ it("returns undefined if no person matches id", async () => {
+ const [{ state, data }, app] = withSetup(() => watchFirst(userTable, "999"));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toBe(null);
+
+ app.unmount();
+ });
+
+ it("updates on changes matching the query", async () => {
+ const [{ state, data }, app] = withSetup(() => watchFirst(userTable, "0"));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ await update(userTable, { ...users[0], name: "Alice the II." });
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual({ ...users[0], name: "Alice the II." });
+
+ app.unmount();
+ });
+});
diff --git a/packages/vue/src/watchFirst.ts b/packages/vue/src/watchFirst.ts
new file mode 100644
index 00000000..8dea5396
--- /dev/null
+++ b/packages/vue/src/watchFirst.ts
@@ -0,0 +1,66 @@
+import { Entity, key, PrimaryKeyOf, Query, Table } from "blinkdb";
+import { computed, ref, ToRefs } from "vue";
+import { QueryResult } from "./types";
+import { watchMany } from "./watchMany";
+
+/**
+ * Retrieves the first entity from `table`.
+ *
+ * @example
+ * // Retrieve the first user
+ * const { data: firstUser } = watchFirst(userTable);
+ */
+export function watchFirst, P extends PrimaryKeyOf>(
+ table: Table
+): ToRefs>;
+
+/**
+ * Retrieves the first entity from `table` matching the given `filter`.
+ *
+ * @example
+ * // Retrieve the first user named 'Alice'
+ * const { data: firstUser } = watchFirst(userTable, {
+ * where: {
+ * name: "Alice"
+ * }
+ * });
+ */
+export function watchFirst, P extends PrimaryKeyOf>(
+ table: Table,
+ query: Query
+): ToRefs>;
+
+/**
+ * Retrieves the first entity from `table` with the given `id`.
+ *
+ * @example
+ * // Retrieve the 'Alice' user by their id
+ * const { data: firstUser } = watchFirst(userTable, 'alice-uuid');
+ */
+export function watchFirst, P extends PrimaryKeyOf>(
+ table: Table,
+ id: T[P]
+): ToRefs>;
+
+export function watchFirst, P extends PrimaryKeyOf>(
+ table: Table,
+ queryOrId?: Query | T[P]
+): ToRefs> {
+ let result: ToRefs>;
+
+ if (queryOrId === undefined) {
+ result = watchMany(table);
+ } else {
+ const query =
+ typeof queryOrId === "object"
+ ? queryOrId
+ : ({ where: { [key(table)]: queryOrId } } as unknown as Query);
+ result = watchMany(table, query);
+ }
+
+ return {
+ state: result.state,
+ data: computed(() => (result.data.value ? result.data.value[0] ?? null : undefined)),
+ error: ref(undefined),
+ } as ToRefs>;
+}
diff --git a/packages/vue/src/watchMany.spec.ts b/packages/vue/src/watchMany.spec.ts
new file mode 100644
index 00000000..caccef60
--- /dev/null
+++ b/packages/vue/src/watchMany.spec.ts
@@ -0,0 +1,138 @@
+import { waitFor } from "@testing-library/vue";
+import { createDB, createTable, insert, insertMany, Table } from "blinkdb";
+import { User, withSetup } from "./testutils";
+import { watchMany } from "./watchMany";
+
+let userTable: Table;
+
+const users: User[] = [
+ { id: "0", name: "Alice" },
+ { id: "1", name: "Bob" },
+ { id: "2", name: "Charlie" },
+];
+
+beforeEach(() => {
+ const db = createDB();
+ userTable = createTable(db, "users")();
+});
+
+test("shows loading state on first render", async () => {
+ const [{ state, data }, app] = withSetup(() => watchMany(userTable));
+
+ expect(state.value).toBe("loading");
+ expect(data.value).toBe(undefined);
+
+ app.unmount();
+});
+
+test("shows done state on subsequent renders", async () => {
+ const [{ state, data }, app] = withSetup(() => watchMany(userTable));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual([]);
+
+ app.unmount();
+});
+
+describe("without filter", () => {
+ beforeEach(async () => {
+ await insertMany(userTable, users);
+ });
+
+ it("returns users", async () => {
+ const [{ state, data }, app] = withSetup(() => watchMany(userTable));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual(users);
+
+ app.unmount();
+ });
+
+ it("updates on changes", async () => {
+ const [{ state, data }, app] = withSetup(() => watchMany(userTable));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ const newUser: User = { id: "4", name: "Delta" };
+ await insert(userTable, newUser);
+
+ await waitFor(() => {
+ expect(data.value).toStrictEqual([...users, newUser]);
+ });
+
+ app.unmount();
+ });
+});
+
+describe("with filter", () => {
+ beforeEach(async () => {
+ await insertMany(userTable, users);
+ });
+
+ it("returns users", async () => {
+ const [{ state, data }, app] = withSetup(() => {
+ return watchMany(userTable, { where: { name: { in: ["Alice", "Bob", "Elise"] } } });
+ });
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ expect(data.value).toStrictEqual(
+ users.filter((u) => ["Alice", "Bob"].includes(u.name))
+ );
+
+ app.unmount();
+ });
+
+ it("doesn't update on changes not matching the query", async () => {
+ const [{ state, data }, app] = withSetup(() => {
+ return watchMany(userTable, { where: { name: { in: ["Alice", "Bob", "Elise"] } } });
+ });
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ const newUser: User = { id: "4", name: "Delta" };
+ await insert(userTable, newUser);
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ expect(data.value).toStrictEqual(
+ users.filter((u) => ["Alice", "Bob"].includes(u.name))
+ );
+
+ app.unmount();
+ });
+
+ it("updates on changes matching the query", async () => {
+ const [{ state, data }, app] = withSetup(() => {
+ return watchMany(userTable, { where: { name: { in: ["Alice", "Bob", "Elise"] } } });
+ });
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ const newUser: User = { id: "4", name: "Elise" };
+ await insert(userTable, newUser);
+
+ await new Promise((res) => setTimeout(res, 100));
+
+ expect(data.value).toStrictEqual([
+ ...users.filter((u) => ["Alice", "Bob"].includes(u.name)),
+ newUser,
+ ]);
+
+ app.unmount();
+ });
+});
diff --git a/packages/vue/src/watchMany.ts b/packages/vue/src/watchMany.ts
new file mode 100644
index 00000000..848f5a3f
--- /dev/null
+++ b/packages/vue/src/watchMany.ts
@@ -0,0 +1,63 @@
+import { Entity, PrimaryKeyOf, Query, Table, watch } from "blinkdb";
+import { computed, onBeforeMount, onBeforeUnmount, ref, ToRefs } from "vue";
+import { QueryResult } from "./types";
+
+/**
+ * Retrieve all entities from `table`.
+ *
+ * @example
+ * const queryResult = watchMany(userTable);
+ */
+export function watchMany, P extends PrimaryKeyOf>(
+ table: Table
+): ToRefs>;
+
+/**
+ * Retrieve all entities from `table` that match the given `filter`.
+ *
+ * @example
+ * // All users called 'Alice'
+ * const queryResult = watchMany(userTable, {
+ * where: {
+ * name: "Alice"
+ * }
+ * });
+ * // All users aged 25 and up
+ * const queryResult = watchMany(userTable, {
+ * where: {
+ * age: { gt: 25 }
+ * }
+ * });
+ */
+export function watchMany, P extends PrimaryKeyOf>(
+ table: Table,
+ query: Query
+): ToRefs>;
+
+export function watchMany, P extends PrimaryKeyOf>(
+ table: Table,
+ query?: Query
+): ToRefs> {
+ const result = ref();
+ let dispose: (() => void) | undefined = undefined;
+
+ onBeforeMount(async () => {
+ if (query) {
+ dispose = await watch(table, query, (items) => {
+ result.value = items;
+ });
+ } else {
+ dispose = await watch(table, (items) => {
+ result.value = items;
+ });
+ }
+ });
+
+ onBeforeUnmount(() => dispose?.());
+
+ return {
+ data: result,
+ state: computed(() => (result.value === undefined ? "loading" : "done")),
+ error: ref(undefined),
+ } as ToRefs>;
+}
diff --git a/packages/vue/src/watchOne.spec.ts b/packages/vue/src/watchOne.spec.ts
new file mode 100644
index 00000000..13eef69c
--- /dev/null
+++ b/packages/vue/src/watchOne.spec.ts
@@ -0,0 +1,185 @@
+import { waitFor } from "@testing-library/vue";
+import { createDB, createTable, insertMany, Table, update } from "blinkdb";
+import { User, withSetup } from "./testutils";
+import { watchOne } from "./watchOne";
+
+let userTable: Table;
+
+const users: User[] = [
+ { id: "0", name: "Alice" },
+ { id: "1", name: "Bob" },
+ { id: "2", name: "Charlie" },
+];
+
+beforeEach(() => {
+ const db = createDB();
+ userTable = createTable(db, "users")();
+});
+
+test("shows loading state on first render", async () => {
+ const [{ state, data }, app] = withSetup(() => watchOne(userTable, "0"));
+
+ expect(state.value).toBe("loading");
+ expect(data.value).toBe(undefined);
+
+ app.unmount();
+});
+
+test("shows done state on subsequent renders", async () => {
+ await insertMany(userTable, users);
+ const [{ state, data }, app] = withSetup(() => watchOne(userTable, "0"));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ app.unmount();
+});
+
+describe("with filter", () => {
+ beforeEach(async () => {
+ await insertMany(userTable, users);
+ });
+
+ it("returns first user matching filter", async () => {
+ const [{ state, data }, app] = withSetup(() =>
+ watchOne(userTable, { where: { name: "Bob" } })
+ );
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual(users[1]);
+
+ app.unmount();
+ });
+
+ it("returns error if no user matches filter", async () => {
+ const [{ state, data, error }, app] = withSetup(() =>
+ watchOne(userTable, { where: { name: "Bobby" } })
+ );
+
+ await waitFor(() => {
+ expect(state.value).toBe("error");
+ });
+ expect(data.value).toBe(undefined);
+ expect(error.value?.message).toMatch(/No item found/);
+
+ app.unmount();
+ });
+
+ it("returns error if more than one user matches filter", async () => {
+ const [{ state, data, error }, app] = withSetup(() =>
+ watchOne(userTable, { where: { name: { in: ["Alice", "Bob"] } } })
+ );
+
+ await waitFor(() => {
+ expect(state.value).toBe("error");
+ });
+ expect(data.value).toBe(undefined);
+ expect(error.value?.message).toMatch(/More than one item found/);
+
+ app.unmount();
+ });
+
+ it("doesn't update on changes not matching the query", async () => {
+ const [{ state, data }, app] = withSetup(() =>
+ watchOne(userTable, { where: { name: "Bob" } })
+ );
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ await update(userTable, { ...users[0], name: "Alice the II." });
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual(users[1]);
+
+ app.unmount();
+ });
+
+ it("updates on changes matching the query", async () => {
+ const [{ state, data }, app] = withSetup(() =>
+ watchOne(userTable, { where: { id: "1" } })
+ );
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ await update(userTable, { ...users[1], name: "Bob the II." });
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual({ ...users[1], name: "Bob the II." });
+
+ app.unmount();
+ });
+});
+
+describe("with id", () => {
+ beforeEach(async () => {
+ await insertMany(userTable, users);
+ });
+
+ it("returns user with the given id", async () => {
+ const [{ state, data }, app] = withSetup(() => watchOne(userTable, "0"));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual(users[0]);
+
+ app.unmount();
+ });
+
+ it("returns error if no user matches id", async () => {
+ const [{ state, data, error }, app] = withSetup(() => watchOne(userTable, "123"));
+
+ await waitFor(() => {
+ expect(state.value).toBe("error");
+ });
+ expect(data.value).toBe(undefined);
+ expect(error.value?.message).toMatch(/No item found/);
+
+ app.unmount();
+ });
+
+ it("updates on changes not matching the id", async () => {
+ const [{ state, data }, app] = withSetup(() => watchOne(userTable, "0"));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ await update(userTable, { ...users[1], name: "Bob the II." });
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual(users[0]);
+
+ app.unmount();
+ });
+
+ it("updates on changes matching the id", async () => {
+ const [{ state, data }, app] = withSetup(() => watchOne(userTable, "0"));
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+
+ await update(userTable, { ...users[0], name: "Alice the II." });
+
+ await waitFor(() => {
+ expect(state.value).toBe("done");
+ });
+ expect(data.value).toStrictEqual({ ...users[0], name: "Alice the II." });
+
+ app.unmount();
+ });
+});
diff --git a/packages/vue/src/watchOne.ts b/packages/vue/src/watchOne.ts
new file mode 100644
index 00000000..3a9a60c9
--- /dev/null
+++ b/packages/vue/src/watchOne.ts
@@ -0,0 +1,80 @@
+import {
+ Entity,
+ ItemNotFoundError,
+ key,
+ MoreThanOneItemFoundError,
+ PrimaryKeyOf,
+ Query,
+ Table,
+} from "blinkdb";
+import { computed, ToRefs } from "vue";
+import { QueryResult } from "./types";
+import { watchMany } from "./watchMany";
+
+/**
+ * Retrieves the entity from `table` matching the given `filter`.
+ *
+ * @throws if no item or more than one item matches the filter.
+ *
+ * @example
+ * // Retrieve the only user named 'Alice'
+ * const queryResult = watchOne(userTable, {
+ * where: {
+ * name: "Alice"
+ * }
+ * });
+ */
+export function watchOne, P extends PrimaryKeyOf>(
+ table: Table,
+ query: Query
+): ToRefs>;
+
+/**
+ * Retrieves an entity from `table` with the given `id`.
+ *
+ * @throws if no item matches the given id.
+ *
+ * @example
+ * // Retrieve the 'Alice' user by their id
+ * const queryResult = watchOne(userTable, 'alice-uuid');
+ */
+export function watchOne, P extends PrimaryKeyOf>(
+ table: Table,
+ id: T[P]
+): ToRefs>;
+
+export function watchOne, P extends PrimaryKeyOf>(
+ table: Table,
+ queryOrId: Query | T[P]
+): ToRefs> {
+ let result: ToRefs>;
+
+ if (queryOrId === undefined) {
+ result = watchMany(table);
+ } else {
+ const query =
+ typeof queryOrId === "object"
+ ? queryOrId
+ : ({ where: { [key(table)]: queryOrId } } as unknown as Query);
+ result = watchMany(table, query);
+ }
+
+ const error = computed(() => {
+ if (result.state.value === "done") {
+ if (result.data.value!.length === 0) {
+ return new ItemNotFoundError(queryOrId);
+ }
+ if (result.data.value!.length > 1) {
+ return new MoreThanOneItemFoundError(queryOrId);
+ }
+ }
+ });
+
+ return {
+ error,
+ state: computed(() => (error.value !== undefined ? "error" : result.state.value)),
+ data: computed(() =>
+ error.value === undefined && result.data.value ? result.data.value[0] : undefined
+ ),
+ } as ToRefs>;
+}
diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json
new file mode 100644
index 00000000..ad768e0e
--- /dev/null
+++ b/packages/vue/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declaration": true
+ },
+ "include": ["**/src/*.ts"]
+}
\ No newline at end of file
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy