From 9a00734633ebab48afb7e35a7574d66ca30c0500 Mon Sep 17 00:00:00 2001 From: stopachka Date: Fri, 13 Dec 2024 16:31:06 -0800 Subject: [PATCH 01/13] [cli] try to log in if we haven't yet --- client/packages/cli/index.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/client/packages/cli/index.js b/client/packages/cli/index.js index 95c1b9c70..596332b07 100644 --- a/client/packages/cli/index.js +++ b/client/packages/cli/index.js @@ -296,7 +296,10 @@ program .command("login") .description("Log into your account") .option("-p --print", "Prints the auth token into the console.") - .action(login); + .action(async (opts) => { + console.log("Let's log you in!"); + await login(opts); + }); program .command("init") @@ -511,7 +514,7 @@ async function login(options) { if (!registerRes.ok) return; const { secret, ticket } = registerRes.data; - console.log("Let's log you in!"); + const ok = await promptOk( `This will open instantdb.com in your browser, OK to proceed?`, ); @@ -534,6 +537,7 @@ async function login(options) { await saveConfigAuthToken(token); console.log(chalk.green(`Successfully logged in as ${email}!`)); } + return token; } async function getOrInstallInstantModuleWithErrorLogging(pkgDir) { @@ -718,7 +722,7 @@ async function resolvePackageAndAuthInfoWithErrorLogging() { if (!instantModuleName) { return; } - const authToken = await readConfigAuthTokenWithErrorLogging(); + const authToken = await readAuthTokenOrLoginWithErrorLogging(); if (!authToken) { return; } @@ -1404,9 +1408,10 @@ async function readConfigAuthToken() { getAuthPaths().authConfigFilePath, "utf-8", ).catch(() => null); - + return authToken; } + async function readConfigAuthTokenWithErrorLogging() { const token = await readConfigAuthToken(); if (!token) { @@ -1417,6 +1422,14 @@ async function readConfigAuthTokenWithErrorLogging() { return token; } +async function readAuthTokenOrLoginWithErrorLogging() { + const token = await readConfigAuthToken(); + if (token) return token; + console.log(`Looks like you are no logged in...`); + console.log(`Let's log in!`); + return await login({}) +} + async function saveConfigAuthToken(authToken) { const authPaths = getAuthPaths(); From f9d48712a68e28d9f0885d8b4aae3a64d1e7f6f9 Mon Sep 17 00:00:00 2001 From: stopachka Date: Fri, 13 Dec 2024 16:32:43 -0800 Subject: [PATCH 02/13] [strong-init] make strong-init the default in packages --- client/packages/admin/src/index.ts | 44 +- client/packages/core/src/index.ts | 327 ++--------- client/packages/core/src/queryTypes.ts | 2 +- client/packages/core/src/schema.ts | 2 +- client/packages/core/src/schemaTypes.ts | 27 +- client/packages/react-native/src/index.ts | 58 +- client/packages/react/src/Cursors.tsx | 2 +- client/packages/react/src/InstantReact.ts | 545 ------------------ .../react/src/InstantReactAbstractDatabase.ts | 11 +- client/packages/react/src/InstantReactWeb.ts | 8 - client/packages/react/src/index.ts | 4 - client/packages/react/src/init.ts | 50 +- client/packages/react/src/useQuery.ts | 64 -- 13 files changed, 161 insertions(+), 983 deletions(-) delete mode 100644 client/packages/react/src/InstantReact.ts delete mode 100644 client/packages/react/src/InstantReactWeb.ts diff --git a/client/packages/admin/src/index.ts b/client/packages/admin/src/index.ts index bb5851475..29ec457ec 100644 --- a/client/packages/admin/src/index.ts +++ b/client/packages/admin/src/index.ts @@ -175,29 +175,47 @@ async function jsonFetch( * Visit https://instantdb.com/dash to get your `appId` :) * * @example - * const db = init({ appId: "my-app-id" }) + * import { init } from "@instantdb/admin" * - * // You can also provide a schema for type safety and editor autocomplete! + * const db = init({ + * appId: "my-app-id", + * adminToken: process.env.INSTANT_ADMIN_TOKEN + * }) * - * type Schema = { - * goals: { - * title: string - * } - * } + * // You can also provide a schema for type safety and editor autocomplete! * - * const db = init({ appId: "my-app-id" }) + * import { init } from "@instantdb/admin" + * import schema from ""../instant.schema.ts"; * + * const db = init({ + * appId: "my-app-id", + * adminToken: process.env.INSTANT_ADMIN_TOKEN, + * schema, + * }) + * // To learn more: https://instantdb.com/docs/modeling-data */ -function init(config: Config) { - return new InstantAdmin(config); -} - -function init_experimental< +function init< Schema extends InstantSchemaDef = InstantUnknownSchema, >(config: InstantConfig) { return new InstantAdminDatabase(config); } +/** + * @deprecated + * `init_experimental` is deprecated. You can replace it with `init`. + * + * @example + * + * // Before + * import { init_experimental } from "@instantdb/admin" + * const db = init_experimental({ ... }); + * + * // After + * import { init } from "@instantdb/admin" + * const db = init({ ... }); + */ +const init_experimental = init; + /** * * The first step: init your application! diff --git a/client/packages/core/src/index.ts b/client/packages/core/src/index.ts index fc907f8ad..a47dacced 100644 --- a/client/packages/core/src/index.ts +++ b/client/packages/core/src/index.ts @@ -171,292 +171,6 @@ function initGlobalInstantCoreStore(): Record { const globalInstantCoreStore = initGlobalInstantCoreStore(); -/** - * - * The first step: init your application! - * - * Visit https://instantdb.com/dash to get your `appId` :) - * - * @example - * const db = init({ appId: "my-app-id" }) - * - * // You can also provide a schema for type safety and editor autocomplete! - * - * type Schema = { - * goals: { - * title: string - * } - * } - * - * const db = init({ appId: "my-app-id" }) - * - */ -function init( - config: Config, - Storage?: any, - NetworkListener?: any, -): InstantCore { - return _init_internal(config, Storage, NetworkListener); -} - -function _init_internal< - Schema extends {} | InstantGraph, - RoomSchema extends RoomSchemaShape, - WithCardinalityInference extends boolean = false, ->( - config: Config, - Storage?: any, - NetworkListener?: any, - versions?: { [key: string]: string }, -): InstantCore { - const existingClient = globalInstantCoreStore[config.appId] as InstantCore< - any, - RoomSchema, - WithCardinalityInference - >; - - if (existingClient) { - return existingClient; - } - - const reactor = new Reactor( - { - ...defaultConfig, - ...config, - }, - Storage || IndexedDBStorage, - NetworkListener || WindowNetworkListener, - versions, - ); - - const client = new InstantCore( - reactor, - ); - globalInstantCoreStore[config.appId] = client; - - if (typeof window !== "undefined" && typeof window.location !== "undefined") { - const showDevtool = - // show widget by default? - ("devtool" in config ? Boolean(config.devtool) : defaultOpenDevtool) && - // only run on localhost (dev env) - window.location.hostname === "localhost" && - // used by dash and other internal consumers - !Boolean((globalThis as any)._nodevtool); - - if (showDevtool) { - createDevtool(config.appId); - } - } - - return client; -} - -class InstantCore< - Schema extends InstantGraph | {} = {}, - RoomSchema extends RoomSchemaShape = {}, - WithCardinalityInference extends boolean = false, -> implements IDatabase -{ - public withCardinalityInference?: WithCardinalityInference; - public _reactor: Reactor; - public auth: Auth; - public storage: Storage; - - public tx = - txInit< - Schema extends InstantGraph ? Schema : InstantGraph - >(); - - constructor(reactor: Reactor) { - this._reactor = reactor; - this.auth = new Auth(this._reactor); - this.storage = new Storage(this._reactor); - } - - /** - * Use this to write data! You can create, update, delete, and link objects - * - * @see https://instantdb.com/docs/instaml - * - * @example - * // Create a new object in the `goals` namespace - * const goalId = id(); - * db.transact(tx.goals[goalId].update({title: "Get fit"})) - * - * // Update the title - * db.transact(tx.goals[goalId].update({title: "Get super fit"})) - * - * // Delete it - * db.transact(tx.goals[goalId].delete()) - * - * // Or create an association: - * todoId = id(); - * db.transact([ - * tx.todos[todoId].update({ title: 'Go on a run' }), - * tx.goals[goalId].link({todos: todoId}), - * ]) - */ - transact( - chunks: TransactionChunk | TransactionChunk[], - ): Promise { - return this._reactor.pushTx(chunks); - } - - getLocalId(name: string): Promise { - return this._reactor.getLocalId(name); - } - - /** - * Use this to query your data! - * - * @see https://instantdb.com/docs/instaql - * - * @example - * // listen to all goals - * db.subscribeQuery({ goals: {} }, (resp) => { - * console.log(resp.data.goals) - * }) - * - * // goals where the title is "Get Fit" - * db.subscribeQuery( - * { goals: { $: { where: { title: "Get Fit" } } } }, - * (resp) => { - * console.log(resp.data.goals) - * } - * ) - * - * // all goals, _alongside_ their todos - * db.subscribeQuery({ goals: { todos: {} } }, (resp) => { - * console.log(resp.data.goals) - * }); - */ - subscribeQuery< - Q extends Schema extends InstantGraph - ? InstaQLParams - : Exactly, - >( - query: Q, - cb: (resp: SubscriptionState) => void, - ) { - return this._reactor.subscribeQuery(query, cb); - } - - /** - * Listen for the logged in state. This is useful - * for deciding when to show a login screen. - * - * @see https://instantdb.com/docs/auth - * @example - * const unsub = db.subscribeAuth((auth) => { - * if (auth.user) { - * console.log('logged in as', auth.user.email) - * } else { - * console.log('logged out') - * } - * }) - */ - subscribeAuth(cb: (auth: AuthResult) => void): UnsubscribeFn { - return this._reactor.subscribeAuth(cb); - } - - /** - * Listen for connection status changes to Instant. This is useful - * for building things like connectivity indicators - * - * @see https://www.instantdb.com/docs/patterns#connection-status - * @example - * const unsub = db.subscribeConnectionStatus((status) => { - * const connectionState = - * status === 'connecting' || status === 'opened' - * ? 'authenticating' - * : status === 'authenticated' - * ? 'connected' - * : status === 'closed' - * ? 'closed' - * : status === 'errored' - * ? 'errored' - * : 'unexpected state'; - * - * console.log('Connection status:', connectionState); - * }); - */ - subscribeConnectionStatus(cb: (status: ConnectionStatus) => void): UnsubscribeFn { - return this._reactor.subscribeConnectionStatus(cb); - } - - /** - * Join a room to publish and subscribe to topics and presence. - * - * @see https://instantdb.com/docs/presence-and-topics - * @example - * // init - * const db = init(); - * const room = db.joinRoom(roomType, roomId); - * // usage - * const unsubscribeTopic = room.subscribeTopic("foo", console.log); - * const unsubscribePresence = room.subscribePresence({}, console.log); - * room.publishTopic("hello", { message: "hello world!" }); - * room.publishPresence({ name: "joe" }); - * // later - * unsubscribePresence(); - * unsubscribeTopic(); - * room.leaveRoom(); - */ - joinRoom( - roomType: RoomType = "_defaultRoomType" as RoomType, - roomId: string = "_defaultRoomId", - ): RoomHandle< - RoomSchema[RoomType]["presence"], - RoomSchema[RoomType]["topics"] - > { - const leaveRoom = this._reactor.joinRoom(roomId); - - return { - leaveRoom, - subscribeTopic: (topic, onEvent) => - this._reactor.subscribeTopic(roomId, topic, onEvent), - subscribePresence: (opts, onChange) => - this._reactor.subscribePresence(roomType, roomId, opts, onChange), - publishTopic: (topic, data) => - this._reactor.publishTopic({ roomType, roomId, topic, data }), - publishPresence: (data) => - this._reactor.publishPresence(roomType, roomId, data), - getPresence: (opts) => this._reactor.getPresence(roomType, roomId, opts), - }; - } - - shutdown() { - delete globalInstantCoreStore[this._reactor.config.appId]; - this._reactor.shutdown(); - } - - /** - * Use this for one-off queries. - * Returns local data if available, otherwise fetches from the server. - * Because we want to avoid stale data, this method will throw an error - * if the user is offline or there is no active connection to the server. - * - * @see https://instantdb.com/docs/instaql - * - * @example - * - * const resp = await db.queryOnce({ goals: {} }); - * console.log(resp.data.goals) - */ - queryOnce< - Q extends Schema extends InstantGraph - ? InstaQLParams - : Exactly, - >( - query: Q, - ): Promise<{ - data: QueryResponse; - pageInfo: PageInfoResponse; - }> { - return this._reactor.queryOnce(query); - } -} - /** * Functions to log users in and out. * @@ -839,12 +553,33 @@ class InstantCoreDatabase> } } -function init_experimental< +/** + * + * The first step: init your application! + * + * Visit https://instantdb.com/dash to get your `appId` :) + * + * @example + * import { init } from "@instantdb/core" + * + * const db = init({ appId: "my-app-id" }) + * + * // You can also provide a schema for type safety and editor autocomplete! + * + * import { init } from "@instantdb/core" + * import schema from ""../instant.schema.ts"; + * + * const db = init({ appId: "my-app-id", schema }) + * + * // To learn more: https://instantdb.com/docs/modeling-data + */ +function init< Schema extends InstantSchemaDef = InstantUnknownSchema, >( config: InstantConfig, Storage?: any, NetworkListener?: any, + versions?: { [key: string]: string }, ): InstantCoreDatabase { const existingClient = globalInstantCoreStore[ config.appId @@ -862,6 +597,7 @@ function init_experimental< }, Storage || IndexedDBStorage, NetworkListener || WindowNetworkListener, + { ...(versions || {}), "@instantdb/core": version }, ); const client = new InstantCoreDatabase(reactor); @@ -897,12 +633,26 @@ type InstantRules = { }; }; +/** + * @deprecated + * `init_experimental` is deprecated. You can replace it with `init`. + * + * @example + * + * // Before + * import { init_experimental } from "@instantdb/core" + * const db = init_experimental({ ... }); + * + * // After + * import { init } from "@instantdb/core" + * const db = init({ ... }); + */ +const init_experimental = init; export { // bada bing bada boom init, init_experimental, - _init_internal, id, tx, txInit, @@ -917,7 +667,6 @@ export { weakHash, IndexedDBStorage, WindowNetworkListener, - InstantCore as InstantClient, InstantCoreDatabase, Auth, Storage, diff --git a/client/packages/core/src/queryTypes.ts b/client/packages/core/src/queryTypes.ts index aedf1fc5b..b4fc60bb5 100644 --- a/client/packages/core/src/queryTypes.ts +++ b/client/packages/core/src/queryTypes.ts @@ -259,7 +259,7 @@ type InstaQLResult< Query extends InstaQLParams, > = Expand<{ [QueryPropName in keyof Query]: QueryPropName extends keyof Schema["entities"] - ? InstaQLEntity[] + ? InstaQLEntity>[] : never; }>; diff --git a/client/packages/core/src/schema.ts b/client/packages/core/src/schema.ts index f00e4f098..c6be3ec70 100644 --- a/client/packages/core/src/schema.ts +++ b/client/packages/core/src/schema.ts @@ -141,7 +141,7 @@ type LinksIndex = Record< * presence, you can define rooms. * * You can push this schema to your database with the CLI, - * or use it inside `init_experimental`, to get typesafety and autocompletion. + * or use it inside `init`, to get typesafety and autocompletion. * * @see https://instantdb.com/docs/schema * @example diff --git a/client/packages/core/src/schemaTypes.ts b/client/packages/core/src/schemaTypes.ts index 4cef43a7e..d36408ac7 100644 --- a/client/packages/core/src/schemaTypes.ts +++ b/client/packages/core/src/schemaTypes.ts @@ -303,6 +303,25 @@ export class InstantSchemaDef< public rooms: Rooms, ) {} + /** + * @deprecated + * `withRoomSchema` is deprecated. Define your schema in `rooms` directly: + * + * @example + * // Before: + * const schema = i.schema({ + * // ... + * }).withRoomSchema() + * + * // After + * const schema = i.schema({ + * rooms: { + * // ... + * } + * }) + * + * @see https://instantdb.com/docs/presence-and-topics#typesafety + */ withRoomSchema<_RoomSchema extends RoomSchemaShape>() { type RDef = RoomDefFromShape<_RoomSchema>; return new InstantSchemaDef( @@ -313,6 +332,12 @@ export class InstantSchemaDef< } } +/** + * @deprecated + * `i.graph` is deprecated. Use `i.schema` instead. + * + * @see https://instantdb.com/docs/modeling-data + */ export class InstantGraph< Entities extends EntitiesDef, Links extends LinksDef, @@ -396,7 +421,7 @@ export type BackwardsCompatibleSchema< export type UnknownEntity = EntityDef< { id: DataAttrDef; - [AttrName: string]: DataAttrDef; + [AttrName: string]: DataAttrDef; }, { [LinkName: string]: LinkAttrDef<"many", string> }, void diff --git a/client/packages/react-native/src/index.ts b/client/packages/react-native/src/index.ts index 2b5eaf8e3..2cb57788a 100644 --- a/client/packages/react-native/src/index.ts +++ b/client/packages/react-native/src/index.ts @@ -6,7 +6,6 @@ import version from "./version"; import { // react - InstantReact, InstantReactAbstractDatabase, // types @@ -29,7 +28,6 @@ import { type InstantQueryResult, type InstantSchema, type InstantSchemaDatabase, - type ConnectionStatus, // schema types @@ -62,43 +60,42 @@ import { * Visit https://instantdb.com/dash to get your `appId` :) * * @example - * const db = init({ appId: "my-app-id" }) + * import { init } from "@instantdb/react-native" * - * // You can also provide a schema for type safety and editor autocomplete! + * const db = init({ appId: "my-app-id" }) * - * type Schema = { - * goals: { - * title: string - * } - * } + * // You can also provide a schema for type safety and editor autocomplete! * - * const db = init({ appId: "my-app-id" }) + * import { init } from "@instantdb/react-native" + * import schema from ""../instant.schema.ts"; * + * const db = init({ appId: "my-app-id", schema }) + * + * // To learn more: https://instantdb.com/docs/modeling-data */ -function init( - config: Config, -) { - return new InstantReactNative(config); -} - -function init_experimental< +function init< Schema extends InstantSchemaDef = InstantUnknownSchema, >(config: InstantConfig) { - return new InstantReactNativeDatabase(config); + return new InstantReactNativeDatabase(config, { + "@instantdb/react-native": version, + }); } -class InstantReactNative< - Schema extends InstantGraph | {} = {}, - RoomSchema extends RoomSchemaShape = {}, - WithCardinalityInference extends boolean = false, -> extends InstantReact { - static Storage = Storage; - static NetworkListener = NetworkListener; - - constructor(config: Config | ConfigWithSchema) { - super(config, { "@instantdb/react-native": version }); - } -} +/** + * @deprecated + * `init_experimental` is deprecated. You can replace it with `init`. + * + * @example + * + * // Before + * import { init_experimental } from "@instantdb/react-native" + * const db = init_experimental({ ... }); + * + * // After + * import { init } from "@instantdb/react-native" + * const db = init({ ... }); + */ +const init_experimental = init; class InstantReactNativeDatabase< Schema extends InstantSchemaDef, @@ -123,7 +120,6 @@ export { type User, type AuthState, type ConnectionStatus, - type InstantReactNative, type InstantQuery, type InstantQueryResult, type InstantSchema, diff --git a/client/packages/react/src/Cursors.tsx b/client/packages/react/src/Cursors.tsx index 2c3662fef..4d1b46b85 100644 --- a/client/packages/react/src/Cursors.tsx +++ b/client/packages/react/src/Cursors.tsx @@ -5,7 +5,7 @@ import { type TouchEvent, type CSSProperties, } from "react"; -import type { InstantReactRoom } from "./InstantReact"; +import type { InstantReactRoom } from "./InstantReactAbstractDatabase"; import type { RoomSchemaShape } from "@instantdb/core"; export function Cursors< diff --git a/client/packages/react/src/InstantReact.ts b/client/packages/react/src/InstantReact.ts deleted file mode 100644 index 3a077dec8..000000000 --- a/client/packages/react/src/InstantReact.ts +++ /dev/null @@ -1,545 +0,0 @@ -import { - // types - InstantClient, - Auth, - Storage, - txInit, - _init_internal, - i, - type AuthState, - type ConnectionStatus, - type Config, - type Query, - type Exactly, - type TransactionChunk, - type LifecycleSubscriptionState, - type PresenceOpts, - type PresenceResponse, - type RoomSchemaShape, - type InstaQLParams, - type ConfigWithSchema, - type IDatabase, - type InstantGraph, - type QueryResponse, - type PageInfoResponse, -} from "@instantdb/core"; -import { - KeyboardEvent, - useCallback, - useEffect, - useMemo, - useRef, - useState, - useSyncExternalStore, -} from "react"; -import { useQuery } from "./useQuery"; -import { useTimeout } from "./useTimeout"; -import version from "./version"; - -export type PresenceHandle< - PresenceShape, - Keys extends keyof PresenceShape, -> = PresenceResponse & { - publishPresence: (data: Partial) => void; -}; - -export type TypingIndicatorOpts = { - timeout?: number | null; - stopOnEnter?: boolean; - // Perf opt - `active` will always be an empty array - writeOnly?: boolean; -}; - -export type TypingIndicatorHandle = { - active: PresenceShape[]; - setActive(active: boolean): void; - inputProps: { - onKeyDown: (e: KeyboardEvent) => void; - onBlur: () => void; - }; -}; - -export const defaultActivityStopTimeout = 1_000; - -export class InstantReactRoom< - Schema extends InstantGraph | {}, - RoomSchema extends RoomSchemaShape, - RoomType extends keyof RoomSchema, -> { - _core: InstantClient; - type: RoomType; - id: string; - - constructor( - _core: InstantClient, - type: RoomType, - id: string, - ) { - this._core = _core; - this.type = type; - this.id = id; - } - - /** - * Listen for broadcasted events given a room and topic. - * - * @see https://instantdb.com/docs/presence-and-topics - * @example - * function App({ roomId }) { - * db.room(roomType, roomId).useTopicEffect("chat", (message, peer) => { - * console.log("New message", message, 'from', peer.name); - * }); - * - * // ... - * } - */ - useTopicEffect = ( - topic: TopicType, - onEvent: ( - event: RoomSchema[RoomType]["topics"][TopicType], - peer: RoomSchema[RoomType]["presence"], - ) => any, - ): void => { - useEffect(() => { - const unsub = this._core._reactor.subscribeTopic( - this.id, - topic, - (event, peer) => { - onEvent(event, peer); - }, - ); - - return unsub; - }, [this.id, topic]); - }; - - /** - * Broadcast an event to a room. - * - * @see https://instantdb.com/docs/presence-and-topics - * @example - * function App({ roomId }) { - * const publishTopic = db.room(roomType, roomId).usePublishTopic("clicks"); - * - * return ( - * - * ); - * } - * - */ - usePublishTopic = ( - topic: Topic, - ): ((data: RoomSchema[RoomType]["topics"][Topic]) => void) => { - useEffect(() => this._core._reactor.joinRoom(this.id), [this.id]); - - const publishTopic = useCallback( - (data) => { - this._core._reactor.publishTopic({ - roomType: this.type, - roomId: this.id, - topic, - data, - }); - }, - [this.id, topic], - ); - - return publishTopic; - }; - - /** - * Listen for peer's presence data in a room, and publish the current user's presence. - * - * @see https://instantdb.com/docs/presence-and-topics - * @example - * function App({ roomId }) { - * const { - * peers, - * publishPresence - * } = db.room(roomType, roomId).usePresence({ keys: ["name", "avatar"] }); - * - * // ... - * } - */ - usePresence = ( - opts: PresenceOpts = {}, - ): PresenceHandle => { - const [state, setState] = useState< - PresenceResponse - >(() => { - return ( - this._core._reactor.getPresence(this.type, this.id, opts) ?? { - peers: {}, - isLoading: true, - } - ); - }); - - useEffect(() => { - const unsub = this._core._reactor.subscribePresence( - this.type, - this.id, - opts, - (data) => { - setState(data); - }, - ); - - return unsub; - }, [this.id, opts.user, opts.peers?.join(), opts.keys?.join()]); - - return { - ...state, - publishPresence: (data) => { - this._core._reactor.publishPresence(this.type, this.id, data); - }, - }; - }; - - /** - * Publishes presence data to a room - * - * @see https://instantdb.com/docs/presence-and-topics - * @example - * function App({ roomId }) { - * db.room(roomType, roomId).useSyncPresence({ name, avatar, color }); - * - * // ... - * } - */ - useSyncPresence = ( - data: Partial, - deps?: any[], - ): void => { - useEffect(() => this._core._reactor.joinRoom(this.id), [this.id]); - useEffect(() => { - return this._core._reactor.publishPresence(this.type, this.id, data); - }, [this.type, this.id, deps ?? JSON.stringify(data)]); - }; - - /** - * Manage typing indicator state - * - * @see https://instantdb.com/docs/presence-and-topics - * @example - * function App({ roomId }) { - * const { - * active, - * setActive, - * inputProps, - * } = db.room(roomType, roomId).useTypingIndicator("chat-input", opts); - * - * return ; - * } - */ - useTypingIndicator = ( - inputName: string, - opts: TypingIndicatorOpts = {}, - ): TypingIndicatorHandle => { - const timeout = useTimeout(); - - const onservedPresence = this.usePresence({ - keys: [inputName], - }); - - const active = useMemo(() => { - const presenceSnapshot = this._core._reactor.getPresence( - this.type, - this.id, - ); - - return opts?.writeOnly - ? [] - : Object.values(presenceSnapshot?.peers ?? {}).filter( - (p) => p[inputName] === true, - ); - }, [opts?.writeOnly, onservedPresence]); - - const setActive = (isActive: boolean) => { - this._core._reactor.publishPresence(this.type, this.id, { - [inputName]: isActive, - } as unknown as Partial); - - if (!isActive) return; - - if (opts?.timeout === null || opts?.timeout === 0) return; - - timeout.set(opts?.timeout ?? defaultActivityStopTimeout, () => { - this._core._reactor.publishPresence(this.type, this.id, { - [inputName]: null, - } as Partial); - }); - }; - - return { - active, - setActive: (a: boolean) => { - setActive(a); - }, - inputProps: { - onKeyDown: (e: KeyboardEvent) => { - const isEnter = opts?.stopOnEnter && e.key === "Enter"; - const isActive = !isEnter; - - setActive(isActive); - }, - onBlur: () => { - setActive(false); - }, - }, - }; - }; -} - -const defaultAuthState = { - isLoading: true, - user: undefined, - error: undefined, -}; - -export abstract class InstantReact< - Schema extends InstantGraph | {} = {}, - RoomSchema extends RoomSchemaShape = {}, - WithCardinalityInference extends boolean = false, -> implements IDatabase -{ - public withCardinalityInference?: WithCardinalityInference; - public tx = - txInit< - Schema extends InstantGraph ? Schema : InstantGraph - >(); - - public auth: Auth; - public storage: Storage; - public _core: InstantClient; - - static Storage?: any; - static NetworkListener?: any; - - constructor( - config: Config | ConfigWithSchema, - versions?: { [key: string]: string }, - ) { - this._core = _init_internal( - config, - // @ts-expect-error because TS can't resolve subclass statics - this.constructor.Storage, - // @ts-expect-error because TS can't resolve subclass statics - this.constructor.NetworkListener, - { ...(versions || {}), "@instantdb/react": version }, - ); - this.auth = this._core.auth; - this.storage = this._core.storage; - } - - getLocalId = (name: string) => { - return this._core.getLocalId(name); - }; - - /** - * Obtain a handle to a room, which allows you to listen to topics and presence data - * - * If you don't provide a `type` or `id`, Instant will default to `_defaultRoomType` and `_defaultRoomId` - * as the room type and id, respectively. - * - * @see https://instantdb.com/docs/presence-and-topics - * - * @example - * const { - * useTopicEffect, - * usePublishTopic, - * useSyncPresence, - * useTypingIndicator, - * } = db.room(roomType, roomId); - */ - room( - type: RoomType = "_defaultRoomType" as RoomType, - id: string = "_defaultRoomId", - ) { - return new InstantReactRoom( - this._core, - type, - id, - ); - } - - /** - * Use this to write data! You can create, update, delete, and link objects - * - * @see https://instantdb.com/docs/instaml - * - * @example - * // Create a new object in the `goals` namespace - * const goalId = id(); - * db.transact(tx.goals[goalId].update({title: "Get fit"})) - * - * // Update the title - * db.transact(tx.goals[goalId].update({title: "Get super fit"})) - * - * // Delete it - * db.transact(tx.goals[goalId].delete()) - * - * // Or create an association: - * todoId = id(); - * db.transact([ - * tx.todos[todoId].update({ title: 'Go on a run' }), - * tx.goals[goalId].link({todos: todoId}), - * ]) - */ - transact = ( - chunks: TransactionChunk | TransactionChunk[], - ) => { - return this._core.transact(chunks); - }; - - /** - * Use this to query your data! - * - * @see https://instantdb.com/docs/instaql - * - * @example - * // listen to all goals - * db.useQuery({ goals: {} }) - * - * // goals where the title is "Get Fit" - * db.useQuery({ goals: { $: { where: { title: "Get Fit" } } } }) - * - * // all goals, _alongside_ their todos - * db.useQuery({ goals: { todos: {} } }) - * - * // skip if `user` is not logged in - * db.useQuery(auth.user ? { goals: {} } : null) - */ - useQuery = < - Q extends Schema extends InstantGraph - ? InstaQLParams - : Exactly, - >( - query: null | Q, - ): LifecycleSubscriptionState => { - return useQuery(this._core, query).state; - }; - - /** - * Listen for the logged in state. This is useful - * for deciding when to show a login screen. - * - * Check out the docs for an example `Login` component too! - * - * @see https://instantdb.com/docs/auth - * @example - * function App() { - * const { isLoading, user, error } = db.useAuth() - * if (isLoading) { - * return
Loading...
- * } - * if (error) { - * return
Uh oh! {error.message}
- * } - * if (user) { - * return
- * } - * return - * } - */ - useAuth = (): AuthState => { - // We use a ref to store the result of the query. - // This is becuase `useSyncExternalStore` uses `Object.is` - // to compare the previous and next state. - // If we don't use a ref, the state will always be considered different, so - // the component will always re-render. - const resultCacheRef = useRef( - this._core._reactor._currentUserCached, - ); - - // Similar to `resultCacheRef`, `useSyncExternalStore` will unsubscribe - // if `subscribe` changes, so we use `useCallback` to memoize the function. - const subscribe = useCallback((cb: Function) => { - const unsubscribe = this._core.subscribeAuth((auth) => { - resultCacheRef.current = { isLoading: false, ...auth }; - cb(); - }); - - return unsubscribe; - }, []); - - const state = useSyncExternalStore( - subscribe, - () => resultCacheRef.current, - () => defaultAuthState, - ); - return state; - }; - - /** - * Listen for connection status changes to Instant. Use this for things like - * showing connection state to users - * - * @see https://www.instantdb.com/docs/patterns#connection-status - * @example - * function App() { - * const status = db.useConnectionStatus() - * const connectionState = - * status === 'connecting' || status === 'opened' - * ? 'authenticating' - * : status === 'authenticated' - * ? 'connected' - * : status === 'closed' - * ? 'closed' - * : status === 'errored' - * ? 'errored' - * : 'unexpected state'; - * - * return
Connection state: {connectionState}
- * } - */ - useConnectionStatus = (): ConnectionStatus => { - const statusRef = useRef(this._core._reactor.status as ConnectionStatus); - - const subscribe = useCallback((cb: Function) => { - const unsubscribe = this._core.subscribeConnectionStatus((newStatus) => { - if (newStatus !== statusRef.current) { - statusRef.current = newStatus; - cb(); - } - }); - - return unsubscribe; - }, []); - - const status = useSyncExternalStore( - subscribe, - () => statusRef.current, - // For SSR, always return 'connecting' as the initial state - () => 'connecting' - ); - - return status; - } - - /** - * Use this for one-off queries. - * Returns local data if available, otherwise fetches from the server. - * Because we want to avoid stale data, this method will throw an error - * if the user is offline or there is no active connection to the server. - * - * @see https://instantdb.com/docs/instaql - * - * @example - * - * const resp = await db.queryOnce({ goals: {} }); - * console.log(resp.data.goals) - */ - queryOnce = < - Q extends Schema extends InstantGraph - ? InstaQLParams - : Exactly, - >( - query: Q, - ): Promise<{ - data: QueryResponse; - pageInfo: PageInfoResponse; - }> => { - return this._core.queryOnce(query); - }; -} diff --git a/client/packages/react/src/InstantReactAbstractDatabase.ts b/client/packages/react/src/InstantReactAbstractDatabase.ts index 6c5d0ea6a..f38348075 100644 --- a/client/packages/react/src/InstantReactAbstractDatabase.ts +++ b/client/packages/react/src/InstantReactAbstractDatabase.ts @@ -3,7 +3,6 @@ import { Auth, Storage, txInit, - _init_internal, type AuthState, type ConnectionStatus, type TransactionChunk, @@ -14,7 +13,7 @@ import { type InstantConfig, type PageInfoResponse, InstantCoreDatabase, - init_experimental, + init as core_init, InstaQLLifecycleState, InstaQLResponse, RoomsOf, @@ -304,13 +303,17 @@ export default abstract class InstantReactAbstractDatabase< static Storage?: any; static NetworkListener?: any; - constructor(config: InstantConfig) { - this._core = init_experimental( + constructor( + config: InstantConfig, + versions?: { [key: string]: string } + ) { + this._core = core_init( config, // @ts-expect-error because TS can't resolve subclass statics this.constructor.Storage, // @ts-expect-error because TS can't resolve subclass statics this.constructor.NetworkListener, + versions, ); this.auth = this._core.auth; this.storage = this._core.storage; diff --git a/client/packages/react/src/InstantReactWeb.ts b/client/packages/react/src/InstantReactWeb.ts deleted file mode 100644 index 671bf6248..000000000 --- a/client/packages/react/src/InstantReactWeb.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { InstantGraph, RoomSchemaShape } from "@instantdb/core"; -import { InstantReact } from "./InstantReact"; - -export class InstantReactWeb< - Schema extends InstantGraph | {} = {}, - RoomSchema extends RoomSchemaShape = {}, - WithCardinalityInference extends boolean = false, -> extends InstantReact {} diff --git a/client/packages/react/src/index.ts b/client/packages/react/src/index.ts index 4830eb8cc..85776f4aa 100644 --- a/client/packages/react/src/index.ts +++ b/client/packages/react/src/index.ts @@ -42,9 +42,7 @@ import { type InstantRules, } from "@instantdb/core"; -import { InstantReact } from "./InstantReact"; import InstantReactAbstractDatabase from "./InstantReactAbstractDatabase"; -import { InstantReactWeb } from "./InstantReactWeb"; import InstantReactWebDatabase from "./InstantReactWebDatabase"; import { init, init_experimental } from "./init"; import { Cursors } from "./Cursors"; @@ -55,13 +53,11 @@ export { lookup, init, init_experimental, - InstantReactWeb, InstantReactWebDatabase, Cursors, i, // internal - InstantReact, InstantReactAbstractDatabase, // types diff --git a/client/packages/react/src/init.ts b/client/packages/react/src/init.ts index d5d237531..1f0dbe8cc 100644 --- a/client/packages/react/src/init.ts +++ b/client/packages/react/src/init.ts @@ -1,14 +1,11 @@ import type { - // types - Config, InstantConfig, - InstantGraph, InstantSchemaDef, - RoomSchemaShape, InstantUnknownSchema, } from "@instantdb/core"; -import { InstantReactWeb } from "./InstantReactWeb"; + import InstantReactWebDatabase from "./InstantReactWebDatabase"; +import version from "./version"; /** * @@ -17,28 +14,39 @@ import InstantReactWebDatabase from "./InstantReactWebDatabase"; * Visit https://instantdb.com/dash to get your `appId` :) * * @example - * const db = init({ appId: "my-app-id" }) + * import { init } from "@instantdb/react" * - * // You can also provide a schema for type safety and editor autocomplete! + * const db = init({ appId: "my-app-id" }) * - * type Schema = { - * goals: { - * title: string - * } - * } + * // You can also provide a schema for type safety and editor autocomplete! * - * const db = init({ appId: "my-app-id" }) + * import { init } from "@instantdb/react" + * import schema from ""../instant.schema.ts"; * + * const db = init({ appId: "my-app-id", schema }) + * + * // To learn more: https://instantdb.com/docs/modeling-data */ export function init< - Schema extends {} = {}, - RoomSchema extends RoomSchemaShape = {}, ->(config: Config) { - return new InstantReactWeb(config); -} - -export function init_experimental< Schema extends InstantSchemaDef = InstantUnknownSchema, >(config: InstantConfig) { - return new InstantReactWebDatabase(config); + return new InstantReactWebDatabase(config, { + "@instantdb/react": version, + }); } + +/** + * @deprecated + * `init_experimental` is deprecated. You can replace it with `init`. + * + * @example + * + * // Before + * import { init_experimental } from "@instantdb/react" + * const db = init_experimental({ ... }); + * + * // After + * import { init } from "@instantdb/react" + * const db = init({ ... }); + */ +export const init_experimental = init; diff --git a/client/packages/react/src/useQuery.ts b/client/packages/react/src/useQuery.ts index 1dfc4fd9e..734a43f60 100644 --- a/client/packages/react/src/useQuery.ts +++ b/client/packages/react/src/useQuery.ts @@ -3,7 +3,6 @@ import { coerceQuery, type Query, type Exactly, - type InstantClient, type LifecycleSubscriptionState, type InstaQLParams, type InstantGraph, @@ -30,69 +29,6 @@ function stateForResult(result: any) { }; } -export function useQuery< - Q extends Schema extends InstantGraph - ? InstaQLParams - : Exactly, - Schema extends InstantGraph | {}, - WithCardinalityInference extends boolean, ->( - _core: InstantClient, - _query: null | Q, -): { - state: LifecycleSubscriptionState; - query: any; -} { - const query = _query ? coerceQuery(_query) : null; - const queryHash = weakHash(query); - - // We use a ref to store the result of the query. - // This is becuase `useSyncExternalStore` uses `Object.is` - // to compare the previous and next state. - // If we don't use a ref, the state will always be considered different, so - // the component will always re-render. - const resultCacheRef = useRef< - LifecycleSubscriptionState - >(stateForResult(_core._reactor.getPreviousResult(query))); - - // Similar to `resultCacheRef`, `useSyncExternalStore` will unsubscribe - // if `subscribe` changes, so we use `useCallback` to memoize the function. - const subscribe = useCallback( - (cb) => { - // Don't subscribe if query is null - if (!query) { - const unsubscribe = () => {}; - return unsubscribe; - } - - const unsubscribe = _core.subscribeQuery(query, (result) => { - resultCacheRef.current = { - isLoading: !Boolean(result), - data: undefined, - pageInfo: undefined, - error: undefined, - ...result, - }; - - cb(); - }); - - return unsubscribe; - }, - // Build a new subscribe function if the query changes - [queryHash, _core], - ); - - const state = useSyncExternalStore< - LifecycleSubscriptionState - >( - subscribe, - () => resultCacheRef.current, - () => defaultState, - ); - return { state, query }; -} - export function useQueryInternal< Q extends InstaQLParams, Schema extends InstantSchemaDef, From 02144aaa7acb2d137baafcdb0bf4d799bc986d85 Mon Sep 17 00:00:00 2001 From: stopachka Date: Fri, 13 Dec 2024 16:34:25 -0800 Subject: [PATCH 03/13] [strong-init] Update callsites update rest of sandbox update calls in dash update pages examples update dash update home page update explorer improve docs styles --- .../admin-sdk-express/src/with-schema.ts | 4 +- client/sandbox/cli-nodejs/instant.schema.ts | 55 ++++++----- .../app/play/colors-schema.js | 4 +- .../react-nextjs/pages/play/checkins.tsx | 48 +++++----- .../sandbox/react-nextjs/pages/play/clerk.tsx | 4 +- .../react-nextjs/pages/play/color-schema.tsx | 62 ------------- .../sandbox/react-nextjs/pages/play/color.tsx | 12 ++- .../react-nextjs/pages/play/cursors.tsx | 2 +- .../react-nextjs/pages/play/ephemeral.tsx | 39 ++++---- .../react-nextjs/pages/play/missing-attrs.tsx | 4 +- .../react-nextjs/pages/play/operators.tsx | 4 +- .../react-nextjs/pages/play/query-like.tsx | 4 +- .../pages/play/query-once-toggle.tsx | 18 ++-- .../react-nextjs/pages/play/query-once.tsx | 16 ++-- .../react-nextjs/pages/play/strong-todos.tsx | 4 +- .../react-nextjs/pages/play/use-query.tsx | 2 +- .../strong-init-vite/instant.schema.ts | 15 +++ client/sandbox/strong-init-vite/src/App.tsx | 4 +- .../src/typescript_tests_experimental.tsx | 16 ++-- .../src/typescript_tests_experimental_v2.tsx | 23 +++-- ...script_tests_experimental_v2_no_schema.tsx | 16 ++-- .../src/typescript_tests_normal.tsx | 16 +++- ...ypescript_tests_normal_as_experimental.tsx | 17 ++-- client/sandbox/vanilla-js-vite/src/schema.ts | 4 +- .../dash/explorer/EditNamespaceDialog.tsx | 8 +- .../dash/explorer/EditRowDialog.tsx | 4 +- .../www/components/dash/explorer/Explorer.tsx | 8 +- .../dash/explorer/QueryInspector.tsx | 10 +- client/www/components/docs/Fence.jsx | 28 +++--- client/www/components/docs/Layout.jsx | 92 +++---------------- client/www/components/docs/Navigation.jsx | 2 +- client/www/components/docs/Prose.jsx | 14 +-- client/www/components/docs/Search.jsx | 2 +- client/www/lib/hooks/explorer.tsx | 6 +- client/www/pages/dash/index.tsx | 4 +- client/www/pages/examples.tsx | 9 +- client/www/pages/examples/1-todos.tsx | 16 +--- client/www/pages/examples/3-cursors.tsx | 7 +- .../www/pages/examples/4-custom-cursors.tsx | 13 +-- client/www/pages/examples/5-reactions.tsx | 19 +--- .../www/pages/examples/6-typing-indicator.tsx | 25 +++-- client/www/pages/examples/7-avatar-stack.tsx | 14 +-- .../www/pages/examples/8-merge-tile-game.tsx | 24 ++--- client/www/pages/index.tsx | 31 ++++--- client/www/styles/globals.css | 14 --- 45 files changed, 291 insertions(+), 452 deletions(-) delete mode 100644 client/sandbox/react-nextjs/pages/play/color-schema.tsx diff --git a/client/sandbox/admin-sdk-express/src/with-schema.ts b/client/sandbox/admin-sdk-express/src/with-schema.ts index 9f84f5f9c..bdc834430 100644 --- a/client/sandbox/admin-sdk-express/src/with-schema.ts +++ b/client/sandbox/admin-sdk-express/src/with-schema.ts @@ -1,7 +1,7 @@ import express from "express"; import bodyParser from "body-parser"; import cors from "cors"; // Import cors module -import { id, i, init_experimental } from "@instantdb/admin"; +import { id, i, init } from "@instantdb/admin"; import { assert } from "console"; import dotenv from "dotenv"; import fs from "fs"; @@ -34,7 +34,7 @@ const schema = i.schema({ rooms: {}, }); -const db = init_experimental({ +const db = init({ apiURI: "http://localhost:8888", appId: process.env.INSTANT_APP_ID!, adminToken: process.env.INSTANT_ADMIN_TOKEN!, diff --git a/client/sandbox/cli-nodejs/instant.schema.ts b/client/sandbox/cli-nodejs/instant.schema.ts index 238e6e9cb..05d52c918 100644 --- a/client/sandbox/cli-nodejs/instant.schema.ts +++ b/client/sandbox/cli-nodejs/instant.schema.ts @@ -2,45 +2,42 @@ import { i } from "@instantdb/core"; const schema = i.schema({ entities: { - authors: i.entity({ - name: i.any(), - userId: i.any(), + profiles: i.entity({ + handle: i.string().unique(), + createdAt: i.date(), }), posts: i.entity({ - content: i.any(), - name: i.any(), + title: i.string(), + body: i.string(), + createdAt: i.date().indexed(), }), tags: i.entity({ - label: i.any(), + title: i.string(), + }), + comments: i.entity({ + body: i.string(), + createdAt: i.date().indexed(), }), }, links: { - authorsPosts: { - forward: { - on: "authors", - has: "many", - label: "posts", - }, - reverse: { - on: "posts", - has: "one", - label: "author", - }, + postAuthor: { + forward: { on: "posts", has: "one", label: "owner" }, + reverse: { on: "profiles", has: "many", label: "posts" }, }, - postsTags: { - forward: { - on: "tags", - has: "many", - label: "posts", - }, - reverse: { - on: "posts", - has: "many", - label: "tags", - }, + commentPost: { + forward: { on: "comments", has: "one", label: "post" }, + reverse: { on: "posts", has: "many", label: "comments" }, }, + commentAuthor: { + forward: { on: "comments", has: "one", label: "author" }, + reverse: { on: "profiles", has: "many", label: "comments" }, + }, + postsTags: { + forward: { on: "posts", has: "many", label: "tags" }, + reverse: { on: "tags", has: "many", label: "posts" }, + } }, - rooms: {}, + rooms: {} }); export default schema; diff --git a/client/sandbox/react-native-expo/app/play/colors-schema.js b/client/sandbox/react-native-expo/app/play/colors-schema.js index f86132f89..dac80b821 100644 --- a/client/sandbox/react-native-expo/app/play/colors-schema.js +++ b/client/sandbox/react-native-expo/app/play/colors-schema.js @@ -1,4 +1,4 @@ -import { init_experimental, i } from "@instantdb/react-native"; +import { init, i } from "@instantdb/react-native"; import { View, Text, Button, StyleSheet } from "react-native"; import config from "../config"; @@ -13,7 +13,7 @@ const schema = i.schema({ rooms: {}, }); -const { useQuery, transact, tx } = init_experimental({ +const { useQuery, transact, tx } = init({ ...config, schema, }); diff --git a/client/sandbox/react-nextjs/pages/play/checkins.tsx b/client/sandbox/react-nextjs/pages/play/checkins.tsx index 20907c419..e46166273 100644 --- a/client/sandbox/react-nextjs/pages/play/checkins.tsx +++ b/client/sandbox/react-nextjs/pages/play/checkins.tsx @@ -1,14 +1,12 @@ import { i, id, - init_experimental, - type InstantSchema, - type InstantQuery, - type InstantQueryResult, - type InstantGraph, - type InstantEntity, - InstantSchemaDatabase, + init, + InstaQLParams, + InstaQLResult, + InstaQLEntity, } from "@instantdb/react"; + import config from "../../config"; interface Data { @@ -16,8 +14,8 @@ interface Data { } const schema = i - .graph( - { + .schema({ + entities: { discriminatedUnionExample: i .entity({ x: i.string(), y: i.number(), z: i.number() }) .asType<{ x: "foo"; y: 1 } | { x: "bar" }>(), @@ -34,7 +32,7 @@ const schema = i name: i.string(), }), }, - { + links: { habitCheckins: { forward: { on: "habits", @@ -60,16 +58,16 @@ const schema = i }, }, }, - ) - .withRoomSchema<{ - demo: { - presence: { - test: number; - }; - }; - }>(); + rooms: { + demo: { + presence: i.entity({ + test: i.number(), + }), + }, + } + }); -const db = init_experimental({ +const db = init({ ...config, schema, }); @@ -146,9 +144,9 @@ const checkinsQuery = { category: {}, }, }, -} satisfies InstantQuery; +} satisfies InstaQLParams; -type CheckinsQueryResult = InstantQueryResult; +type CheckinsQueryResult = InstaQLResult; const result: CheckinsQueryResult = { checkins: [ @@ -176,12 +174,8 @@ const deepVal = result.checkins[0].habit?.category?.id; // types type DeepVal = typeof deepVal; -type Graph = InstantGraph; -type DBGraph = InstantSchema; -type DB2 = InstantSchemaDatabase; - -type Checkin = InstantEntity< - DB2, +type Checkin = InstaQLEntity< + typeof schema, "checkins", { habit: { diff --git a/client/sandbox/react-nextjs/pages/play/clerk.tsx b/client/sandbox/react-nextjs/pages/play/clerk.tsx index 318cd77ae..8b5ceaebe 100644 --- a/client/sandbox/react-nextjs/pages/play/clerk.tsx +++ b/client/sandbox/react-nextjs/pages/play/clerk.tsx @@ -8,10 +8,10 @@ import { UserButton, } from "@clerk/nextjs"; -import { init, tx, id, InstantReactWeb } from "@instantdb/react"; +import { init, InstantReactWebDatabase } from "@instantdb/react"; import config from "../../config"; -function App({ db }: { db: InstantReactWeb<{}, {}> }) { +function App({ db }: { db: InstantReactWebDatabase }) { const { getToken, signOut } = useAuth(); const signInWithToken = () => { getToken().then((jwt) => { diff --git a/client/sandbox/react-nextjs/pages/play/color-schema.tsx b/client/sandbox/react-nextjs/pages/play/color-schema.tsx deleted file mode 100644 index 5e8632eeb..000000000 --- a/client/sandbox/react-nextjs/pages/play/color-schema.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { i, init_experimental } from "@instantdb/react"; -import { useEffect } from "react"; -import config from "../../config"; - -const db = init_experimental({ - ...config, - schema: i.schema({ - entities: { - colors: i.entity({ - color: i.string().indexed(), - }), - }, - links: {}, - rooms: {}, - }), -}); - -function App() { - return
; -} - -const selectId = "4d39508b-9ee2-48a3-b70d-8192d9c5a059"; - -function Main() { - useEffect(() => { - (async () => { - const id = await db.getLocalId("user"); - console.log("localId", id); - })(); - }, []); - const { isLoading, error, data } = db.useQuery({ - colors: {}, - }); - if (isLoading) return
Loading...
; - if (error) return
Error: {error.message}
; - const { colors } = data; - const { color } = colors[0] || { color: "grey" }; - return ( -
-
-

Hi! pick your favorite color

-
- {["green", "blue", "purple"].map((c) => { - return ( - - ); - })} -
-
-
- ); -} - -export default App; diff --git a/client/sandbox/react-nextjs/pages/play/color.tsx b/client/sandbox/react-nextjs/pages/play/color.tsx index dde08c09b..4188d469e 100644 --- a/client/sandbox/react-nextjs/pages/play/color.tsx +++ b/client/sandbox/react-nextjs/pages/play/color.tsx @@ -1,8 +1,16 @@ -import { init, tx } from "@instantdb/react"; +import { i, init, tx } from "@instantdb/react"; import { useEffect } from "react"; import config from "../../config"; -const db = init(config); +const schema = i.schema({ + entities: { + colors: i.entity({ color: i.string() }), + }, + links: {}, + rooms: {}, +}); + +const db = init({ ...config, schema }); function App() { return
; diff --git a/client/sandbox/react-nextjs/pages/play/cursors.tsx b/client/sandbox/react-nextjs/pages/play/cursors.tsx index c23c7fd8f..1d9d891c7 100644 --- a/client/sandbox/react-nextjs/pages/play/cursors.tsx +++ b/client/sandbox/react-nextjs/pages/play/cursors.tsx @@ -3,7 +3,7 @@ import Head from "next/head"; import { init, Cursors } from "@instantdb/react"; import config from "../../config"; -const db = init<{}, any>(config); +const db = init(config); const room = db.room("main", "123"); function App() { diff --git a/client/sandbox/react-nextjs/pages/play/ephemeral.tsx b/client/sandbox/react-nextjs/pages/play/ephemeral.tsx index ef463de42..d879e445a 100644 --- a/client/sandbox/react-nextjs/pages/play/ephemeral.tsx +++ b/client/sandbox/react-nextjs/pages/play/ephemeral.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; import Head from "next/head"; -import { Cursors, init } from "@instantdb/react"; +import { Cursors, i, init } from "@instantdb/react"; import config from "../../config"; const DemoFlags = { @@ -13,26 +13,29 @@ const roomId = "demo-8"; const name = `user ${Date.now()}`; -const db = init< - {}, - { +const schema = i.schema({ + entities: {}, + links: {}, + rooms: { "demo-room": { - presence: { - test: number; - name: string; - cursor?: { x: number; y: number }; - }; + presence: i.entity({ + test: i.number(), + name: i.string(), + cursor: i.json<{ x: number; y: number }>(), + }), topics: { - testTopic: { test: number }; - }; - }; + testTopic: i.entity({ test: i.number() }), + }, + }, "demo-room-2": { - presence: { - test: number; - }; - }; - } ->(config); + presence: i.entity({ + test: i.number(), + }), + }, + }, +}); + +const db = init({ ...config, schema }); const room = db.room("demo-room", roomId); const { diff --git a/client/sandbox/react-nextjs/pages/play/missing-attrs.tsx b/client/sandbox/react-nextjs/pages/play/missing-attrs.tsx index 6db3f0f07..48e457b5d 100644 --- a/client/sandbox/react-nextjs/pages/play/missing-attrs.tsx +++ b/client/sandbox/react-nextjs/pages/play/missing-attrs.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import config from "../../config"; -import { init, init_experimental, tx, id, i } from "@instantdb/react"; +import { init, tx, id, i } from "@instantdb/react"; import { useRouter } from "next/router"; const schema = i.schema({ @@ -32,7 +32,7 @@ const schema = i.schema({ function Example({ appId, useSchema }: { appId: string; useSchema: boolean }) { const myConfig = { ...config, appId }; const db = useSchema - ? init_experimental({ ...myConfig, schema }) + ? init({ ...myConfig, schema }) : (init(myConfig) as any); const q = db.useQuery({ comments: {} }); const [attrs, setAttrs] = useState(); diff --git a/client/sandbox/react-nextjs/pages/play/operators.tsx b/client/sandbox/react-nextjs/pages/play/operators.tsx index 5e6ce81d2..a541809da 100644 --- a/client/sandbox/react-nextjs/pages/play/operators.tsx +++ b/client/sandbox/react-nextjs/pages/play/operators.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import config from "../../config"; -import { init_experimental, tx, id, i } from "@instantdb/react"; +import { init, tx, id, i } from "@instantdb/react"; import { useRouter } from "next/router"; const schema = i.schema({ @@ -42,7 +42,7 @@ const d = new Date(); function Example({ appId }: { appId: string }) { const router = useRouter(); const myConfig = { ...config, appId, schema }; - const db = init_experimental(myConfig); + const db = init(myConfig); const { data } = db.useQuery({ comments: { diff --git a/client/sandbox/react-nextjs/pages/play/query-like.tsx b/client/sandbox/react-nextjs/pages/play/query-like.tsx index c8bea5ff4..2d96e1149 100644 --- a/client/sandbox/react-nextjs/pages/play/query-like.tsx +++ b/client/sandbox/react-nextjs/pages/play/query-like.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import config from "../../config"; -import { init_experimental, tx, id, i } from "@instantdb/react"; +import { init, tx, id, i } from "@instantdb/react"; import { useRouter } from "next/router"; const schema = i.schema({ @@ -32,7 +32,7 @@ const schema = i.schema({ function Example({ appId }: { appId: string }) { const router = useRouter(); const myConfig = { ...config, appId, schema }; - const db = init_experimental(myConfig); + const db = init(myConfig); const { data } = db.useQuery({ items: {} }); diff --git a/client/sandbox/react-nextjs/pages/play/query-once-toggle.tsx b/client/sandbox/react-nextjs/pages/play/query-once-toggle.tsx index f84b5afd3..fe5ccd3b6 100644 --- a/client/sandbox/react-nextjs/pages/play/query-once-toggle.tsx +++ b/client/sandbox/react-nextjs/pages/play/query-once-toggle.tsx @@ -8,17 +8,23 @@ * */ -import { init, id, tx } from "@instantdb/react"; +import { init, i } from "@instantdb/react"; import Head from "next/head"; import { useEffect, FormEvent, useRef, useState } from "react"; import config from "../../config"; -const db = init<{ - onceTest: { - text: string; - }; -}>({ +const schema = i.schema({ + entities: { + onceTest: i.entity({ text: i.string() }), + posts: i.entity({ title: i.string() }), + }, + links: {}, + rooms: {}, +}); + +const db = init({ ...config, + schema }); function useEffectOnce(cb: () => void) { diff --git a/client/sandbox/react-nextjs/pages/play/query-once.tsx b/client/sandbox/react-nextjs/pages/play/query-once.tsx index ab6d33707..78e8aa56b 100644 --- a/client/sandbox/react-nextjs/pages/play/query-once.tsx +++ b/client/sandbox/react-nextjs/pages/play/query-once.tsx @@ -5,16 +5,20 @@ * to validate if the item already exists. * */ -import { init, id, tx } from "@instantdb/react"; +import { init, id, tx, i } from "@instantdb/react"; import Head from "next/head"; import { useEffect, FormEvent } from "react"; import config from "../../config"; -const db = init<{ - onceTest: { - text: string; - }; -}>(config); +const schema = i.schema({ + entities: { + onceTest: i.entity({ text: i.string() }), + }, + links: {}, + rooms: {}, +}); + +const db = init({...config, schema}); function _subsCount() { return Object.values(db._core._reactor.queryOnceDfds).flat().length; diff --git a/client/sandbox/react-nextjs/pages/play/strong-todos.tsx b/client/sandbox/react-nextjs/pages/play/strong-todos.tsx index 21b46c29d..d714edd23 100644 --- a/client/sandbox/react-nextjs/pages/play/strong-todos.tsx +++ b/client/sandbox/react-nextjs/pages/play/strong-todos.tsx @@ -1,6 +1,6 @@ import { i, - init_experimental, + init, InstaQLEntity, type InstaQLParams, } from "@instantdb/react"; @@ -38,7 +38,7 @@ type _AppSchema = typeof _schema; interface AppSchema extends _AppSchema {} const schema: AppSchema = _schema; -const db = init_experimental({ +const db = init({ ...config, schema, }); diff --git a/client/sandbox/react-nextjs/pages/play/use-query.tsx b/client/sandbox/react-nextjs/pages/play/use-query.tsx index e8dfc5734..08cf78b96 100644 --- a/client/sandbox/react-nextjs/pages/play/use-query.tsx +++ b/client/sandbox/react-nextjs/pages/play/use-query.tsx @@ -35,7 +35,7 @@ export default function () { if (selectedPersonId) { return; } - setSelectedPersonId(peopleRes.data?.people[0].id ?? null); + setSelectedPersonId(peopleRes.data?.people[0]?.id ?? null); }, [peopleRes.data]); return ( diff --git a/client/sandbox/strong-init-vite/instant.schema.ts b/client/sandbox/strong-init-vite/instant.schema.ts index 798a4a201..073393efe 100644 --- a/client/sandbox/strong-init-vite/instant.schema.ts +++ b/client/sandbox/strong-init-vite/instant.schema.ts @@ -14,6 +14,9 @@ const _schema = i.schema({ title: i.string(), body: i.string(), }), + messages: i.entity({ + content: i.string(), + }), }, // You can define links here. // For example, if `posts` should have many `comments`. @@ -32,6 +35,18 @@ const _schema = i.schema({ label: "ownedPosts", }, }, + messageCreator: { + forward: { + on: "messages", + has: "one", + label: "creator", + }, + reverse: { + on: "$users", + has: "many", + label: "createdMessages", + }, + }, }, rooms: { chat: { diff --git a/client/sandbox/strong-init-vite/src/App.tsx b/client/sandbox/strong-init-vite/src/App.tsx index 60033bccd..74a8f7e63 100644 --- a/client/sandbox/strong-init-vite/src/App.tsx +++ b/client/sandbox/strong-init-vite/src/App.tsx @@ -1,7 +1,7 @@ -import { id, init_experimental } from "@instantdb/react"; +import { id, init } from "@instantdb/react"; import schema from "../instant.schema.v2"; -const db = init_experimental({ +const db = init({ appId: import.meta.env.VITE_INSTANT_APP_ID, schema, apiURI: "http://localhost:8888", diff --git a/client/sandbox/strong-init-vite/src/typescript_tests_experimental.tsx b/client/sandbox/strong-init-vite/src/typescript_tests_experimental.tsx index 37bf989e7..7f19f0c58 100644 --- a/client/sandbox/strong-init-vite/src/typescript_tests_experimental.tsx +++ b/client/sandbox/strong-init-vite/src/typescript_tests_experimental.tsx @@ -1,15 +1,15 @@ import { id, - init_experimental as core_init_experimental, + init as core_init, InstantQuery, InstantEntity, InstantQueryResult, InstantSchema, InstantSchemaDatabase, } from "@instantdb/core"; -import { init_experimental as react_init_experimental } from "@instantdb/react"; -import { init_experimental as react_native_init_experimental } from "@instantdb/react-native"; -import { init_experimental as admin_init_experimental } from "@instantdb/admin"; +import { init as react_init } from "@instantdb/react"; +import { init as react_native_init } from "@instantdb/react-native"; +import { init as admin_init } from "@instantdb/admin"; import graph from "../instant.schema"; type EmojiName = "fire" | "wave" | "confetti" | "heart"; @@ -33,7 +33,7 @@ type Rooms = { // ---- // Core -const coreDB = core_init_experimental({ +const coreDB = core_init({ appId: import.meta.env.VITE_INSTANT_APP_ID, schema: graph.withRoomSchema(), }); @@ -112,7 +112,7 @@ fromSchemaDBWorks; // // ---- // // React -const reactDB = react_init_experimental({ +const reactDB = react_init({ appId: import.meta.env.VITE_INSTANT_APP_ID, schema: graph.withRoomSchema(), }); @@ -186,7 +186,7 @@ reactSchema; // ---- // React-Native -const reactNativeDB = react_native_init_experimental({ +const reactNativeDB = react_native_init({ appId: import.meta.env.VITE_INSTANT_APP_ID, schema: graph.withRoomSchema(), }); @@ -258,7 +258,7 @@ rnSchema; // ---- // Admin -const adminDB = admin_init_experimental({ +const adminDB = admin_init({ appId: import.meta.env.VITE_INSTANT_APP_ID!, adminToken: import.meta.env.VITE_INSTANT_ADMIN_TOKEN!, schema: graph, diff --git a/client/sandbox/strong-init-vite/src/typescript_tests_experimental_v2.tsx b/client/sandbox/strong-init-vite/src/typescript_tests_experimental_v2.tsx index 87a04464f..5dda1685b 100644 --- a/client/sandbox/strong-init-vite/src/typescript_tests_experimental_v2.tsx +++ b/client/sandbox/strong-init-vite/src/typescript_tests_experimental_v2.tsx @@ -1,19 +1,19 @@ import { id, - init_experimental as core_init_experimental, + init as core_init, InstaQLParams, InstaQLEntity, InstaQLResult, } from "@instantdb/core"; -import { init_experimental as react_init_experimental } from "@instantdb/react"; -import { init_experimental as react_native_init_experimental } from "@instantdb/react-native"; -import { init_experimental as admin_init_experimental } from "@instantdb/admin"; +import { init as react_init } from "@instantdb/react"; +import { init as react_native } from "@instantdb/react-native"; +import { init as admin_init } from "@instantdb/admin"; import schema, { AppSchema } from "../instant.schema.v2"; // ---- // Core -const coreDB = core_init_experimental({ +const coreDB = core_init({ appId: import.meta.env.VITE_INSTANT_APP_ID, schema, }); @@ -47,7 +47,7 @@ coreDB.tx.messages[id()] // ---- // React -const reactDB = react_init_experimental({ +const reactDB = react_init({ appId: import.meta.env.VITE_INSTANT_APP_ID, schema, }); @@ -87,7 +87,7 @@ function ReactNormalApp() { // ---- // React-Native -const reactNativeDB = react_native_init_experimental({ +const reactNativeDB = react_native({ appId: import.meta.env.VITE_INSTANT_APP_ID, schema: schema, }); @@ -119,7 +119,7 @@ function ReactNativeNormalApp() { // ---- // Admin -const adminDB = admin_init_experimental({ +const adminDB = admin_init({ appId: import.meta.env.VITE_INSTANT_APP_ID!, adminToken: import.meta.env.VITE_INSTANT_ADMIN_TOKEN!, schema, @@ -196,6 +196,9 @@ type DeeplyNestedResultWorks = InstaQLResult< AppSchema, { messages: { + $: { + limit: 10; + }; creator: { createdMessages: { creator: {}; @@ -214,8 +217,8 @@ type DeeplyNestedResultFailsBadInput = InstaQLResult< messages: { creator: { createdMessages: { - // Type '{ foo: {}; }' is not assignable to type - // '$Option | ($Option & InstaQLQuerySubqueryParams) + // Type '{ foo: {}; }' is not assignable to type + // '$Option | ($Option & InstaQLQuerySubqueryParams) // | undefined' foo: {}; }; diff --git a/client/sandbox/strong-init-vite/src/typescript_tests_experimental_v2_no_schema.tsx b/client/sandbox/strong-init-vite/src/typescript_tests_experimental_v2_no_schema.tsx index 2634b0d54..530cee5e6 100644 --- a/client/sandbox/strong-init-vite/src/typescript_tests_experimental_v2_no_schema.tsx +++ b/client/sandbox/strong-init-vite/src/typescript_tests_experimental_v2_no_schema.tsx @@ -1,19 +1,19 @@ import { id, - init_experimental as core_init_experimental, + init as core_init, InstaQLParams, InstaQLEntity, InstaQLResult, InstantUnknownSchema, } from "@instantdb/core"; -import { init_experimental as react_init_experimental } from "@instantdb/react"; -import { init_experimental as react_native_init_experimental } from "@instantdb/react-native"; -import { init_experimental as admin_init_experimental } from "@instantdb/admin"; +import { init as react_init } from "@instantdb/react"; +import { init as react_native_init } from "@instantdb/react-native"; +import { init as admin_init } from "@instantdb/admin"; // ---- // Core -const coreDB = core_init_experimental({ +const coreDB = core_init({ appId: import.meta.env.VITE_INSTANT_APP_ID, }); @@ -46,7 +46,7 @@ coreDB.tx.posts[id()] // ---- // React -const reactDB = react_init_experimental({ +const reactDB = react_init({ appId: import.meta.env.VITE_INSTANT_APP_ID, }); @@ -85,7 +85,7 @@ function ReactNormalApp() { // ---- // React-Native -const reactNativeDB = react_native_init_experimental({ +const reactNativeDB = react_native_init({ appId: import.meta.env.VITE_INSTANT_APP_ID, }); @@ -117,7 +117,7 @@ function ReactNativeNormalApp() { // ---- // Admin -const adminDB = admin_init_experimental({ +const adminDB = admin_init({ appId: import.meta.env.VITE_INSTANT_APP_ID!, adminToken: import.meta.env.VITE_INSTANT_ADMIN_TOKEN!, }); diff --git a/client/sandbox/strong-init-vite/src/typescript_tests_normal.tsx b/client/sandbox/strong-init-vite/src/typescript_tests_normal.tsx index d4e2beb12..1ab1218d3 100644 --- a/client/sandbox/strong-init-vite/src/typescript_tests_normal.tsx +++ b/client/sandbox/strong-init-vite/src/typescript_tests_normal.tsx @@ -1,4 +1,8 @@ -import { id, init as core_init } from "@instantdb/core"; +import { + id, + init as core_init, + BackwardsCompatibleSchema, +} from "@instantdb/core"; import { init as react_init } from "@instantdb/react"; import { init as react_native_init } from "@instantdb/react-native"; import { init as admin_init } from "@instantdb/admin"; @@ -37,7 +41,7 @@ type Rooms = { // ---- // Core -const coreDB = core_init({ +const coreDB = core_init>({ appId: import.meta.env.VITE_INSTANT_APP_ID, }); @@ -67,7 +71,7 @@ coreDB.tx.messages[id()] // ---- // React -const reactDB = react_init({ +const reactDB = react_init>({ appId: import.meta.env.VITE_INSTANT_APP_ID, }); @@ -103,7 +107,9 @@ function ReactNormalApp() { // ---- // React-Native -const reactNativeDB = react_native_init({ +const reactNativeDB = react_native_init< + BackwardsCompatibleSchema +>({ appId: import.meta.env.VITE_INSTANT_APP_ID, }); @@ -133,7 +139,7 @@ function ReactNativeNormalApp() { // ---- // Admin -const adminDB = admin_init({ +const adminDB = admin_init>({ appId: import.meta.env.VITE_INSTANT_APP_ID!, adminToken: import.meta.env.VITE_INSTANT_ADMIN_TOKEN!, }); diff --git a/client/sandbox/strong-init-vite/src/typescript_tests_normal_as_experimental.tsx b/client/sandbox/strong-init-vite/src/typescript_tests_normal_as_experimental.tsx index 23a4e5eb1..6e6c6ab50 100644 --- a/client/sandbox/strong-init-vite/src/typescript_tests_normal_as_experimental.tsx +++ b/client/sandbox/strong-init-vite/src/typescript_tests_normal_as_experimental.tsx @@ -1,11 +1,15 @@ import { id, - init_experimental as core_init, - BackwardsCompatibleSchema, + init as core_init, + init_experimental as _a, + BackwardsCompatibleSchema, } from "@instantdb/core"; -import { init_experimental as react_init } from "@instantdb/react"; -import { init_experimental as react_native_init } from "@instantdb/react-native"; -import { init_experimental as admin_init } from "@instantdb/admin"; +import { init as react_init, init_experimental as _b } from "@instantdb/react"; +import { + init as react_native_init, + init_experimental as _c, +} from "@instantdb/react-native"; +import { init as admin_init, init_experimental as _d } from "@instantdb/admin"; type Message = { content: string; @@ -38,7 +42,8 @@ type Rooms = { }; }; -type SchemaDef =BackwardsCompatibleSchema; +type SchemaDef = BackwardsCompatibleSchema; + // ---- // Core diff --git a/client/sandbox/vanilla-js-vite/src/schema.ts b/client/sandbox/vanilla-js-vite/src/schema.ts index ba4881bb5..d72265510 100644 --- a/client/sandbox/vanilla-js-vite/src/schema.ts +++ b/client/sandbox/vanilla-js-vite/src/schema.ts @@ -1,5 +1,5 @@ import "./style.css"; -import { init_experimental, i, id } from "@instantdb/core"; +import { init, i, id } from "@instantdb/core"; const APP_ID = import.meta.env.VITE_INSTANT_APP_ID; @@ -14,7 +14,7 @@ buttonEl.onclick = () => { }; document.body.appendChild(buttonEl); -const db = init_experimental({ +const db = init({ appId: APP_ID, schema: i.schema({ entities: { diff --git a/client/www/components/dash/explorer/EditNamespaceDialog.tsx b/client/www/components/dash/explorer/EditNamespaceDialog.tsx index 51c399875..e53b2a516 100644 --- a/client/www/components/dash/explorer/EditNamespaceDialog.tsx +++ b/client/www/components/dash/explorer/EditNamespaceDialog.tsx @@ -1,5 +1,5 @@ import { id } from '@instantdb/core'; -import { InstantReactWeb } from '@instantdb/react'; +import { InstantReactWebDatabase } from '@instantdb/react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { ArrowLeftIcon, PlusIcon, TrashIcon } from '@heroicons/react/solid'; import { errorToast, successToast } from '@/lib/toast'; @@ -43,7 +43,7 @@ export function EditNamespaceDialog({ isSystemCatalogNs, pushNavStack, }: { - db: InstantReactWeb; + db: InstantReactWebDatabase; appId: string; namespace: SchemaNamespace; namespaces: SchemaNamespace[]; @@ -212,7 +212,7 @@ function AddAttrForm({ namespaces, onClose, }: { - db: InstantReactWeb; + db: InstantReactWebDatabase; namespace: SchemaNamespace; namespaces: SchemaNamespace[]; onClose: () => void; @@ -1035,7 +1035,7 @@ function EditAttrForm({ isSystemCatalogNs, pushNavStack, }: { - db: InstantReactWeb; + db: InstantReactWebDatabase; appId: string; attr: SchemaAttr; onClose: () => void; diff --git a/client/www/components/dash/explorer/EditRowDialog.tsx b/client/www/components/dash/explorer/EditRowDialog.tsx index fe6a7a004..e89806e6a 100644 --- a/client/www/components/dash/explorer/EditRowDialog.tsx +++ b/client/www/components/dash/explorer/EditRowDialog.tsx @@ -1,4 +1,4 @@ -import { id, InstantReactWeb, tx } from '@instantdb/react'; +import { id, InstantReactWebDatabase, tx } from '@instantdb/react'; import { useState } from 'react'; import { @@ -98,7 +98,7 @@ export function EditRowDialog({ item, onClose, }: { - db: InstantReactWeb; + db: InstantReactWebDatabase; namespace: SchemaNamespace; item: Record; onClose: () => void; diff --git a/client/www/components/dash/explorer/Explorer.tsx b/client/www/components/dash/explorer/Explorer.tsx index 8f2d3474e..5e77f7b76 100644 --- a/client/www/components/dash/explorer/Explorer.tsx +++ b/client/www/components/dash/explorer/Explorer.tsx @@ -1,5 +1,5 @@ import { id, tx } from '@instantdb/core'; -import { InstantReactWeb } from '@instantdb/react'; +import { InstantReactWebDatabase } from '@instantdb/react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { isObject } from 'lodash'; import produce from 'immer'; @@ -47,7 +47,7 @@ export function Explorer({ db, appId, }: { - db: InstantReactWeb; + db: InstantReactWebDatabase; appId: string; }) { // DEV @@ -832,7 +832,7 @@ function NewNamespaceDialog({ db, onClose, }: { - db: InstantReactWeb; + db: InstantReactWebDatabase; onClose: (p?: { id: string; name: string }) => void; }) { const [name, setName] = useState(''); @@ -886,7 +886,7 @@ export type PushNavStack = (nav: ExplorerNav) => void; // DEV -function _dev(db: InstantReactWeb) { +function _dev(db: InstantReactWebDatabase) { if (typeof window !== 'undefined') { const i = { db, diff --git a/client/www/components/dash/explorer/QueryInspector.tsx b/client/www/components/dash/explorer/QueryInspector.tsx index 9a18d3678..b6f3d4c09 100644 --- a/client/www/components/dash/explorer/QueryInspector.tsx +++ b/client/www/components/dash/explorer/QueryInspector.tsx @@ -1,6 +1,5 @@ -import { InstantReactWeb } from '@instantdb/react'; import JsonParser from 'json5'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import * as Tooltip from '@radix-ui/react-tooltip'; import { StarIcon, TrashIcon } from '@heroicons/react/outline'; import { ArrowSmRightIcon } from '@heroicons/react/solid'; @@ -8,6 +7,7 @@ import { ArrowSmRightIcon } from '@heroicons/react/solid'; import { Button, CodeEditor, cn } from '@/components/ui'; import { useSchemaQuery } from '@/lib/hooks/explorer'; import { errorToast, infoToast } from '@/lib/toast'; +import { InstantReactWebDatabase } from '@instantdb/react'; const SAVED_QUERIES_CACHE_KEY = '__instant:explorer-saved-queries'; const QUERY_HISTORY_CACHE_KEY = '__instant:explorer-query-history'; @@ -61,7 +61,7 @@ export function QueryInspector({ }: { className?: string; appId: string; - db: InstantReactWeb; + db: InstantReactWebDatabase; }) { const cache = new QueryInspectorCache(appId); const [query, setQuery] = useState>({}); @@ -193,11 +193,11 @@ export function QueryInspector({ // cmd+S/cmd+Enter bindings to run query editor.addCommand( monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, - () => run(editor.getValue()) + () => run(editor.getValue()), ); editor.addCommand( monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, - () => run(editor.getValue()) + () => run(editor.getValue()), ); }} /> diff --git a/client/www/components/docs/Fence.jsx b/client/www/components/docs/Fence.jsx index 9e3be7485..5445bc8e6 100644 --- a/client/www/components/docs/Fence.jsx +++ b/client/www/components/docs/Fence.jsx @@ -16,7 +16,7 @@ export function Fence({ children, language, showCopy }) { '// Instant app', app ? `// ID for app: ${app.title}` - : `// Visit https://instantdb.com/dash to get your APP_ID :)` + : `// Visit https://instantdb.com/dash to get your APP_ID :)`, ) .replace('__APP_ID__', app ? app.id : '__APP_ID__'); @@ -28,20 +28,18 @@ export function Fence({ children, language, showCopy }) { theme={undefined} > {({ className, style, tokens, getTokenProps }) => ( -
+
-            
-              {tokens.map((line, lineIndex) => (
-                
-                  {line
-                    .filter((token) => !token.empty)
-                    .map((token, tokenIndex) => (
-                      
-                    ))}
-                  {'\n'}
-                
-              ))}
-            
+            {tokens.map((line, lineIndex) => (
+              
+                {line
+                  .filter((token) => !token.empty)
+                  .map((token, tokenIndex) => (
+                    
+                  ))}
+                {'\n'}
+              
+            ))}
           
{showCopy && (
@@ -54,7 +52,7 @@ export function Fence({ children, language, showCopy }) { }, 2500); }} className="flex items-center gap-x-1 - rounded-md bg-white px-2 py-1 text-xs font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" + bg-white px-2 py-1 text-xs font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" > { - if (tableOfContents.length === 0) return; - let headings = getHeadings(tableOfContents); - - function onScroll() { - if (!scrollContainerRef.current) return; - - let top = scrollContainerRef.current.scrollTop; - let current = headings[0].id; - for (let heading of headings) { - if (top >= heading.top) { - current = heading.id; - } else { - break; - } - } - - setCurrentSection(current); - } - - function getHeadings() { - return tableOfContents - .flatMap((node) => [node.id, ...node.children.map((child) => child.id)]) - .map((id) => { - let el = document.getElementById(id); - if (!el) return; - - let top = el.offsetTop; - return { id, top }; - }) - .filter((_) => _); - } - - window.addEventListener('scroll', onScroll, true); - onScroll(); - - return () => { - window.removeEventListener('scroll', onScroll, true); - }; - }, [tableOfContents, scrollContainerRef]); - - return currentSection; -} - function useSelectedApp(apps = []) { const cacheKey = 'docs-appId'; const router = useRouter(); @@ -127,7 +80,7 @@ function AppPicker({ apps, selectedAppData, updateSelectedAppId }) { } return ( -
+

Pick your app

The examples below will be updated with your app ID. @@ -176,17 +129,6 @@ export function Layout({ children, title, tableOfContents }) { let section = navigation.find((section) => section.links.find((link) => link.href === router.pathname), ); - let currentSection = useTableOfContents(tableOfContents, scrollContainerRef); - - function isActive(section) { - if (section.id === currentSection) { - return true; - } - if (!section.children) { - return false; - } - return section.children.findIndex(isActive) > -1; - } const token = useAuthToken(); const dashResponse = useTokenFetch(`${config.apiURI}/dash`, token); @@ -219,23 +161,19 @@ export function Layout({ children, title, tableOfContents }) {

{(title || section) && ( -
+
{section && ( -

- {section.title} -

+

{section.title}

)} {title && ( -

- {title} -

+

{title}

)}
)} @@ -245,13 +183,13 @@ export function Layout({ children, title, tableOfContents }) {
{previousPage && (
-
+
Previous
{' '} {previousPage.title} @@ -261,13 +199,13 @@ export function Layout({ children, title, tableOfContents }) { )} {nextPage && (
-
+
Next
{nextPage.title} @@ -276,7 +214,7 @@ export function Layout({ children, title, tableOfContents }) { )}
-
+
- ) + ); } // Write Data // --------- function addTodo(text: string) { db.transact( - tx.todos[id()].update({ + db.tx.todos[id()].update({ text, done: false, createdAt: Date.now(), }) - ) + ); } function deleteTodo(todo: Todo) { - db.transact(tx.todos[todo.id].delete()) + db.transact(db.tx.todos[todo.id].delete()); } function toggleDone(todo: Todo) { - db.transact(tx.todos[todo.id].update({ done: !todo.done })) + db.transact(db.tx.todos[todo.id].update({ done: !todo.done })); } function deleteCompleted(todos: Todo[]) { - const completed = todos.filter((todo) => todo.done) - const txs = completed.map((todo) => tx.todos[todo.id].delete()) - db.transact(txs) + const completed = todos.filter((todo) => todo.done); + const txs = completed.map((todo) => db.tx.todos[todo.id].delete()); + db.transact(txs); } function toggleAll(todos: Todo[]) { - const newVal = !todos.every((todo) => todo.done) - db.transact(todos.map((todo) => tx.todos[todo.id].update({ done: newVal }))) + const newVal = !todos.every((todo) => todo.done); + db.transact(todos.map((todo) => db.tx.todos[todo.id].update({ done: newVal }))); } // Components @@ -104,9 +112,9 @@ function TodoForm({ todos }: { todos: Todo[] }) {
{ - e.preventDefault() - addTodo(e.target[0].value) - e.target[0].value = '' + e.preventDefault(); + addTodo(e.target[0].value); + e.target[0].value = ""; }} >
- ) + ); } function TodoList({ todos }: { todos: Todo[] }) { @@ -134,7 +142,7 @@ function TodoList({ todos }: { todos: Todo[] }) { />
{todo.done ? ( - + {todo.text} ) : ( @@ -147,113 +155,103 @@ function TodoList({ todos }: { todos: Todo[] }) {
))}
- ) + ); } function ActionBar({ todos }: { todos: Todo[] }) { return (
Remaining todos: {todos.filter((todo) => !todo.done).length}
-
deleteCompleted(todos)}> +
deleteCompleted(todos)}> Delete Completed
- ) -} - -// Types -// ---------- -type Todo = { - id: string - text: string - done: boolean - createdAt: number + ); } // Styles // ---------- const styles: Record = { container: { - boxSizing: 'border-box', - backgroundColor: '#fafafa', - fontFamily: 'code, monospace', - height: '100vh', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - flexDirection: 'column', + boxSizing: "border-box", + fontFamily: "code, monospace", + height: "100vh", + display: "flex", + justifyContent: "center", + alignItems: "center", + flexDirection: "column", }, header: { - letterSpacing: '2px', - fontSize: '50px', - color: 'lightgray', - marginBottom: '10px', + letterSpacing: "2px", + fontSize: "50px", + color: "lightgray", + marginBottom: "10px", }, form: { - boxSizing: 'inherit', - display: 'flex', - border: '1px solid lightgray', - borderBottomWidth: '0px', - width: '350px', + boxSizing: "inherit", + display: "flex", + border: "1px solid lightgray", + borderBottomWidth: "0px", + width: "350px", }, toggleAll: { - fontSize: '30px', - cursor: 'pointer', - marginLeft: '11px', - marginTop: '-6px', - width: '15px', - marginRight: '12px', + fontSize: "30px", + cursor: "pointer", + marginLeft: "11px", + marginTop: "-6px", + width: "15px", + marginRight: "12px", }, input: { - backgroundColor: 'transparent', - fontFamily: 'code, monospace', - width: '287px', - padding: '10px', - fontStyle: 'italic', + backgroundColor: "transparent", + fontFamily: "code, monospace", + width: "287px", + padding: "10px", + fontStyle: "italic", }, todoList: { - boxSizing: 'inherit', - width: '350px', + boxSizing: "inherit", + width: "350px", }, checkbox: { - fontSize: '30px', - marginLeft: '5px', - marginRight: '20px', - cursor: 'pointer', + fontSize: "30px", + marginLeft: "5px", + marginRight: "20px", + cursor: "pointer", }, todo: { - display: 'flex', - alignItems: 'center', - padding: '10px', - border: '1px solid lightgray', - borderBottomWidth: '0px', + display: "flex", + alignItems: "center", + padding: "10px", + border: "1px solid lightgray", + borderBottomWidth: "0px", }, todoText: { - flexGrow: '1', - overflow: 'hidden', + flexGrow: "1", + overflow: "hidden", }, delete: { - width: '25px', - cursor: 'pointer', - color: 'lightgray', + width: "25px", + cursor: "pointer", + color: "lightgray", }, actionBar: { - display: 'flex', - justifyContent: 'space-between', - width: '328px', - padding: '10px', - border: '1px solid lightgray', - fontSize: '10px', + display: "flex", + justifyContent: "space-between", + width: "328px", + padding: "10px", + border: "1px solid lightgray", + fontSize: "10px", }, footer: { - marginTop: '20px', - fontSize: '10px', + marginTop: "20px", + fontSize: "10px", }, -} +}; -export default App +export default App; ``` Go to `localhost:3000` and follow the final instruction to load the app! -Huzzah 🎉 You've got your first Instant web app running! Check out the [**Explore**](/docs/init) section to learn more about how to use Instant :) +Huzzah 🎉 You've got your first Instant web app running! Check out the [Working with data](/docs/init) section to learn more about how to use Instant :) diff --git a/client/www/pages/docs/init.md b/client/www/pages/docs/init.md index ae768eb5e..8c2fd75de 100644 --- a/client/www/pages/docs/init.md +++ b/client/www/pages/docs/init.md @@ -8,7 +8,7 @@ The first step to using Instant in your app is to call `init` before rendering y import { init } from '@instantdb/react'; // Instant app -const APP_ID = '__APP_ID__'; +const APP_ID = "__APP_ID__"; const db = init({ appId: APP_ID }); @@ -17,35 +17,31 @@ function App() { } ``` -If you're using TypeScript, `init` accepts a `Schema` generic, which will provide auto-completion and type-safety for `useQuery` results. +With that, you can use `db` to [write data](/docs/instaml), [make queries](/docs/instaql), [handle auth](/docs/auth), and more! + +## Typesafety + +If you're using typescript, `init` accepts a `schema` argument. Adding a schema provides auto-completion and typesafety for your queries and transactions. ```typescript -import { init } from '@instantdb/react'; +import { init, i } from '@instantdb/react'; // Instant app -const APP_ID = '__APP_ID__'; - -type MyAppSchema = { - metrics: { - name: string; - description: string; - }; - logs: { - date: string; - value: number; - unit: string; - }; -}; - -const db = init({ appId: APP_ID }); +const APP_ID = "__APP_ID__"; + +const schema = i.schema({ + entities: { + todos: i.entity({ + text: i.string(), + done: i.boolean(), + createdAt: i.number(), + }), + }, + links: {}, + rooms: {}, +}); + +const db = init({ appId: APP_ID, schema }); ``` -You'll now be able to use `InstaQL` and `InstalML` throughout your app! - -{% callout type="note" %} - -**Psst: Schema-as-code and type safety!** - -Instant now supports a [CLI-based workflow](/docs/cli), managing your [schema as code](/docs/schema), and [strictly-typed queries and mutations](/docs/strong-init). Give them a whirl and let us know what you think! - -{% /callout %} +To learn more about writing schemas, head on over to the [Modeling your data](/docs/modeling-data) section. diff --git a/client/www/pages/docs/instaml.md b/client/www/pages/docs/instaml.md index 98d4603d0..e2706ef92 100644 --- a/client/www/pages/docs/instaml.md +++ b/client/www/pages/docs/instaml.md @@ -8,8 +8,15 @@ Instant uses a **Firebase-inspired** interface for mutations. We call our mutati We use the `update` action to create entities. -```javascript -db.transact([tx.goals[id()].update({ title: 'eat' })]); +```typescript +import { init } from '@instantdb/react'; + +const db = init({ + appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!, +}); + +// transact! 🔥 +db.transact(db.tx.goals[id()].update({ title: 'eat' })); ``` This creates a new `goal` with the following properties: @@ -20,25 +27,25 @@ This creates a new `goal` with the following properties: Similar to NoSQL, you don't need to use the same schema for each entity in a namespace. After creating the previous goal you can run the following: ```javascript -db.transact([ - tx.goals[id()].update({ +db.transact( + db.tx.goals[id()].update({ priority: 'none', isSecret: true, value: 10, aList: [1, 2, 3], anObject: { foo: 'bar' }, }), -]); +); ``` You can store `strings`, `numbers`, `booleans`, `arrays`, and `objects` as values. You can also generate values via functions. Below is an example for picking a random goal title. ```javascript -db.transact([ - tx.goals[id()].update({ +db.transact( + db.tx.goals[id()].update({ title: ['eat', 'sleep', 'hack', 'repeat'][Math.floor(Math.random() * 4)], }), -]); +); ``` --- @@ -47,97 +54,64 @@ The `update` action is also used for updating entities. Suppose we had created t ```javascript const eatId = id(); -db.transact([ - tx.goals[eatId].update({ priority: 'top', lastTimeEaten: 'Yesterday' }), -]); +db.transact( + db.tx.goals[eatId].update({ priority: 'top', lastTimeEaten: 'Yesterday' }), +); ``` We eat some food and decide to update the goal. We can do that like so: ```javascript -db.transact([tx.goals[eatId].update({ lastTimeEaten: 'Today' })]); +db.transact(db.tx.goals[eatId].update({ lastTimeEaten: 'Today' })); ``` This will only update the value of the `lastTimeEaten` attribute for entity `eat`. ## Merge data -When you use `update`, you overwrite the entire entity. This is fine for updating +When you `update` an attribute, you overwrite it. This is fine for updating values of strings, numbers, and booleans. But if you use `update` to overwrite json objects you may encounter two problems: 1. You lose any data you didn't specify. 2. You risk clobbering over changes made by other clients. -To make working with deeply-nested, document-style JSON values a breeze, we created `merge`. -Similar to [lodash's `merge` function](https://lodash.com/docs/4.17.15#merge), -`merge` allows you to specify the slice of data you want to update. +For example, imagine we had a `game` entity, that stored a `state` of favorite colors: ```javascript -// We have a 4x4 tile clicking game with different colors -// and we want to update a specific cell in the game -// from blue to red -const game = { - '0-0': 'red', - '0-1': 'blue', - '0-2': 'green', - '0-3': 'green', - '1-0': 'green', - '1-1': 'red', - '1-2': 'blue', - '1-3': 'green', - '2-0': 'green', - '2-1': 'green', - '2-2': 'red', - '2-3': 'blue', - '3-0': 'blue', - '3-1': 'blue', - '3-2': 'green', - '3-3': 'red', -}; +// User 1 saves {'0-0': 'red'} +db.transact(db.tx.games[gameId].update({ state: { '0-0': 'red' } })); -const boardId = '83c059e2-ed47-42e5-bdd9-6de88d26c521'; -const row = 0; -const col = 1; -const myColor = 'red'; - -// ✅✅ Use `merge` -// With `merge` we can specify the exact cell we want to update -// and only send that data to the server. This way we don't risk -// overwriting other changes made by other clients. -transact([ - tx.boards[boardId].merge({ - state: { - [`${row}-${col}`]: myColor, - }, - }), -]); +// User 2 saves {'0-1': 'blue'} +db.transact(db.tx.games[gameId].update({ state: { '0-1': 'blue' } })); + +// 🤔 Uh oh! User 2 overwrite User 1: +// Final State: {'0-1': 'blue' } ``` -`merge` only merges objects. Calling `merge` on **arrays, numbers, or booleans** will overwrite the values. +To make working with deeply-nested, document-style JSON values a breeze, we created `merge`. +Similar to [lodash's `merge` function](https://lodash.com/docs/4.17.15#merge), +`merge` allows you to specify the slice of data you want to update: ```javascript -// Initial state: {num: 1, arr: [1, 2, 3], bool: true, text: 'hello', obj: {a: 1, b: 2}} -const randomId = '83c059e2-ed47-42e5-bdd9-6de88d26c521'; -transact([ - tx.keys[randomId].merge({ state: { num: 2 } }), // will overwrite num from 1 -> 2 - tx.keys[randomId].merge({ state: { arr: [4] }), // will overwrite arr from [1, 2, 3] -> [4] - tx.keys[randomId].merge({ state: { bool: false } }), // will overwrite bool from true -> false - tx.keys[randomId].merge({ state: { text: 'world' } }), // will overwrite text from 'hello' -> 'world' - tx.keys[randomId].merge({ state: { obj: { c: 3 } } }), // will merge obj from {a: 1, b: 2} -> {a: 1, b: 2, c: 3} -]); +// User 1 saves {'0-0': 'red'} +db.transact(db.tx.games[gameId].merge({ state: { '0-0': 'red' } })); + +// User 2 saves {'0-1': 'blue'} +db.transact(db.tx.games[gameId].merge({ state: { '0-1': 'blue' } })); + +// ✅ Wohoo! Both states are merged! +// Final State: {'0-0': 'red', '0-0': 'blue' } ``` +`merge` only merges objects. Calling `merge` on **arrays, numbers, or booleans** will overwrite the values. + Sometimes you may want to remove keys from a nested object. You can do so by calling `merge` with a key set to `null` or `undefined`. This will remove the corresponding property from the object. ```javascript -// Initial state: { obj: { a: 1, b: 2 } } -const randomId = '83c059e2-ed47-42e5-bdd9-6de88d26c521'; -transact([ - tx.keys[randomId].merge({ state: { obj: { a: null } } }), // will delete key `a` from `state.obj` -]); -// End state: { obj: { b: 2 } } -// `state.obj.a` has been removed +// State: {'0-0': 'red', '0-0': 'blue' } +db.transact(db.tx.games[gameId].merge({ state: { '0-1': null } })); +// New State! {'0-0': 'red' } ``` ## Delete data @@ -145,16 +119,17 @@ transact([ The `delete` action is used for deleting entities. ```javascript -db.transact([tx.goals[eatId].delete()]); +db.transact(db.tx.goals[eatId].delete()); ``` You can generate an array of `delete` txs to delete all entities in a namespace ```javascript -const { isLoading, error, data } = db.useQuery({goals: {}} +const { isLoading, error, data } = db.useQuery({ goals: {} }); const { goals } = data; -... -db.transact(goals.map(g => tx.goals[g.id].delete())); +// ... + +db.transact(goals.map((g) => db.tx.goals[g.id].delete())); ``` Calling `delete` on an entity also deletes its associations. So no need to worry about cleaning up previously created links. @@ -167,15 +142,15 @@ Suppose we create a `goal` and a `todo`. ```javascript db.transact([ - tx.todos[workoutId].update({ title: 'Go on a run' }), - tx.goals[healthId].update({ title: 'Get fit!' }), + db.tx.todos[workoutId].update({ title: 'Go on a run' }), + db.tx.goals[healthId].update({ title: 'Get fit!' }), ]); ``` We can associate `healthId` with `workoutId` like so: ```javascript -db.transact([tx.goals[healthId].link({ todos: workoutId })]); +db.transact(tx.goals[healthId].link({ todos: workoutId })); ``` We could have done all this in one `transact` too via chaining transaction chunks. @@ -191,10 +166,10 @@ You can specify multiple ids in one `link` as well: ```javascript db.transact([ - tx.todos[workoutId].update({ title: 'Go on a run' }), - tx.todos[proteinId].update({ title: 'Drink protein' }), - tx.todos[sleepId].update({ title: 'Go to bed early' }), - tx.goals[healthId] + db.tx.todos[workoutId].update({ title: 'Go on a run' }), + db.tx.todos[proteinId].update({ title: 'Drink protein' }), + db.tx.todos[sleepId].update({ title: 'Go to bed early' }), + db.tx.goals[healthId] .update({ title: 'Get fit!' }) .link({ todos: [workoutId, proteinId, sleepId] }), ]); @@ -203,7 +178,7 @@ db.transact([ Links are bi-directional. Say we link `healthId` to `workoutId` ```javascript -db.transact([tx.goals[healthId].link({ todos: workoutId })]); +db.transact(tx.goals[healthId].link({ todos: workoutId })); ``` We can query associations in both directions @@ -224,14 +199,13 @@ console.log('todos with nested goals', todos); Links can be removed via `unlink.` ```javascript -db.transact([tx.goals[healthId].unlink({ todos: workoutId })]); +db.transact(tx.goals[healthId].unlink({ todos: workoutId })); ``` -This removes links in both directions. Unlinking can be done in either direction so unlinking `workoutId` from `healthId` -would have the same effect. +This removes links in both directions. Unlinking can be done in either direction so unlinking `workoutId` from `healthId` would have the same effect. ```javascript -db.transact([tx.todos[workoutId].unlink({ goals: healthId })]); +db.transact([db.tx.todos[workoutId].unlink({ goals: healthId })]); ``` We can `unlink` multiple ids too: @@ -248,11 +222,13 @@ db.transact([ If your entity has a unique attribute, you can use `lookup` in place of the id to perform updates. ```javascript -import { lookup } from '@instantdb/core'; +import { lookup } from '@instantdb/react'; -db.transact([ - tx.users[lookup('email', 'max@example.com')].update({ name: 'Max' }), -]); +db.transact( + db.tx.profiles[lookup('email', 'eva_lu_ator@instantdb.com')].update({ + name: 'Eva Lu Ator', + }), +); ``` The `lookup` function takes the attribute as its first argument and the unique attribute value as its second argument. @@ -264,14 +240,61 @@ It can be used with `update`, `delete`, `merge`, `link`, and `unlink`. When used with links, it can also be used in place of the linked entity's id. ```javascript -db.transact([ - tx.users[lookup('email', 'max@example.com')].link({ +db.transact( + tx.users[lookup('email', 'eva_lu_ator@instantdb.com')].link({ posts: lookup('number', 15), }), -]); +); +``` + +## Typesafety + +By default, `db.transact` is permissive. When you save data, we'll create missing attributes for you: + +```typescript +db.tx.todos[workoutId].update({ + // Instant will automatically create this attribute + dueDate: Date.now() + 60 * 1000, +}); ``` +As your app grows, you may want to start enforcing types. When you're ready, you can start using a [schema](/docs/modeling-data): + +```typescript +import { init } from '@instantdb/react'; + +import schema from '../instant.schema.ts'; + +const db = init({ + appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!, + schema, +}); +``` + +If your schema includes a `todos.dueDate` for example: + +```typescript +// instant.schema.ts + +const _schema = i.schema({ + entities: { + todos: i.entity({ + // ... + dueDate: i.date(), + }), + }, + // ... +}); +``` + +Instant will enforce that `todos.dueDate` are actually dates, and you'll get intellisense to boot: + +{% screenshot src="https://paper-attachments.dropboxusercontent.com/s_3D2DA1E694B2F8E030AC1EC0B7C47C6AC1E40485744489E3189C95FCB5181D4A_1734122951978_duedate1.png" /%} + +To learn more about writing schemas, check out the [Modeling Data](/docs/modeling-data) section. + ## Batching transactions + If you have a large number of transactions to commit, you'll want to batch them to avoid hitting transaction limits and time outs. @@ -286,7 +309,9 @@ const createGoals = async (total) => { // iterate through all your goals and create batches for (let i = 0; i < total; i++) { const goalNumber = i + 1; - goals.push(tx.goals[id()].update({goalNumber, title: `Goal ${goalNumber}`})); + goals.push( + db.tx.goals[id()].update({ goalNumber, title: `Goal ${goalNumber}` }), + ); // We have enough goals to create a batch if (goals.length >= batchSize) { @@ -302,17 +327,17 @@ const createGoals = async (total) => { // Now that you have your batches, transact them for (const batch of batches) { - await transact(batch); + await db.transact(batch); } -} +}; ``` ## Using the tx proxy object -`tx` is a [proxy object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) which creates transaction chunks to be committed via `db.transact`. It follows the format +`db.tx` is a [proxy object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) which creates transaction chunks to be committed via `db.transact`. It follows the format ``` -tx.NAMESPACE_LABEL[ENTITY_IDENTIFIER].ACTION(ACTION_SPECIFIC_DATA) +db.tx.NAMESPACE_LABEL[ENTITY_IDENTIFIER].ACTION(ACTION_SPECIFIC_DATA) ``` - `NAMESPACE_LABEL` refers to the namespace to commit (e.g. `goals`, `todos`) diff --git a/client/www/pages/docs/instaql.md b/client/www/pages/docs/instaql.md index 68cad3370..3c324b711 100644 --- a/client/www/pages/docs/instaql.md +++ b/client/www/pages/docs/instaql.md @@ -9,8 +9,18 @@ Instant uses a declarative syntax for querying. It's like GraphQL without the co One of the simplest queries you can write is to simply get all entities of a namespace. ```javascript -const query = { goals: {} }; -const { isLoading, error, data } = db.useQuery(query); +import { init } from '@instantdb/react'; + +const db = init({ + appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!, +}); + +function App() { + // Queries! 🚀 + const query = { goals: {} }; + const { isLoading, error, data } = db.useQuery(query); + // ... +} ``` Inspecting `data`, we'll see: @@ -148,17 +158,17 @@ console.log(data) The SQL equivalent for this would be something along the lines of: ```javascript -const query = " -SELECT g.*, gt.todos -FROM goals g -JOIN ( - SELECT g.id, json_agg(t.*) as todos - FROM goals g - LEFT JOIN todos t on g.id = t.goal_id - GROUP BY 1 -) gt on g.id = gt.id -" -const data = {goals: doSQL(query)} +const query = ` + SELECT g.*, gt.todos + FROM goals g + JOIN ( + SELECT g.id, json_agg(t.*) as todos + FROM goals g + LEFT JOIN todos t on g.id = t.goal_id + GROUP BY 1 + ) gt on g.id = gt.id +`; +const data = { goals: doSQL(query) }; ``` Notice the complexity of this SQL query. Although fetching associations in SQL is straightforward via `JOIN`, marshalling the results in a nested structure via SQL is tricky. An alternative approach would be to write two straight-forward queries and then marshall the data on the client. @@ -687,7 +697,7 @@ console.log(data) The `where` clause supports comparison operators on fields that are indexed and have checked types. {% callout %} -Add indexes and checked types to your attributes from the [Explorer on the Instant dashboard](/dash?t=explorer) or from the [cli with Schema-as-code](/docs/schema). +Add indexes and checked types to your attributes from the [Explorer on the Instant dashboard](/dash?t=explorer) or from the [cli with Schema-as-code](/docs/modeling-data). {% /callout %} | Operator | Description | JS equivalent | @@ -901,12 +911,11 @@ For **case insensitive** matching use `$ilike` in place of `$like`. Here's how you can do queries like `startsWith`, `endsWith` and `includes`. -| Example | Description | JS equivalent | +| Example | Description | JS equivalent | | :-----------------------: | :-------------------: | :-----------: | -| `{ $like: "Get%" }` | Starts with 'Get' | `startsWith` | -| `{ $like: "%promoted!" }` | Ends with 'promoted!' | `endsWith` | -| `{ $like: "%fit%" }` | Contains 'fit' | `includes` | - +| `{ $like: "Get%" }` | Starts with 'Get' | `startsWith` | +| `{ $like: "%promoted!" }` | Ends with 'promoted!' | `endsWith` | +| `{ $like: "%fit%" }` | Contains 'fit' | `includes` | Here's how you can use `$like` to find all goals that end with the word "promoted!" @@ -998,6 +1007,118 @@ console.log(data) } ``` +## Typesafety + +By default, `db.useQuery` is permissive. You don't have to tell us your schema upfront, and you can write any kind of query: + +```typescript +const query = { + goals: { + todos: {}, + }, +}; +const { isLoading, error, data } = db.useQuery(query); +``` + +As your app grows, you may want to start enforcing types. When you're ready you can write a [schema](/docs/modeling-data): + +```typescript +import { init } from '@instantdb/react'; + +import schema from '../instant.schema.ts'; + +const db = init({ + appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!, + schema, +}); +``` + +If your schema includes `goals` and `todos` for example: + +```typescript +// instant.schema.ts + +import { i } from '@instantdb/core'; + +const _schema = i.schema({ + entities: { + goals: i.entity({ + title: i.string(), + }), + todos: i.entity({ + title: i.string(), + dueDate: i.date(), + }), + }, + links: { + goalsTodos: { + forward: { on: 'todos', has: 'many', label: 'goals' }, + reverse: { on: 'goals', has: 'many', label: 'todos' }, + }, + }, + rooms: {}, +}); + +// This helps Typescript display better intellisense +type _AppSchema = typeof _schema; +interface AppSchema extends _AppSchema {} +const schema: AppSchema = _schema; + +export type { AppSchema }; +export default schema; +``` + +### Intellisense + +Instant will start giving you intellisense for your queries. For example, if you're querying for goals, you'll see that only `todos` can be associated: + +{% screenshot src="https://paper-attachments.dropboxusercontent.com/s_3D2DA1E694B2F8E030AC1EC0B7C47C6AC1E40485744489E3189C95FCB5181D4A_1734124125680_goals.png" /%} + +And if you hover over `data`, you'll see the actual typed output of your query: + +{% screenshot src="https://paper-attachments.dropboxusercontent.com/s_3D2DA1E694B2F8E030AC1EC0B7C47C6AC1E40485744489E3189C95FCB5181D4A_1734126726346_data.png" /%} + +### Utility Types + +Instant also comes with some utility types to help you use your schema in TypeScript. + +For example, you could define your `query` upfront: + +```typescript +import { InstaQLParams } from '@instantdb/react'; +import { AppSchema } from '../instant.schema.ts'; + +// `query` typechecks against our schema! +const query = { + goals: { todos: {} }, +} satisfies InstaQLParams; +``` + +Or you can define your result type: + +```typescript +import { InstaQLResult } from '@instantdb/react'; +import { AppSchema } from '../instant.schema.ts'; + +type GoalsTodosResult = InstaQLResult< + AppSchema, + { goals: { todos: {} } } +>; +``` + +Or you can extract a particular entity: + +```typescript +import { InstaQLEntity } from '@instantdb/react'; +import { AppSchema } from '../instant.schema.ts'; + +type Todo = InstaQLEntity< + AppSchema, + 'todos' +>; +``` + +To learn more about writing schemas, check out the [Modeling Data](/docs/modeling-data) section. ## Query once diff --git a/client/www/pages/docs/modeling-data.md b/client/www/pages/docs/modeling-data.md index 73eb32006..abd2f8352 100644 --- a/client/www/pages/docs/modeling-data.md +++ b/client/www/pages/docs/modeling-data.md @@ -2,189 +2,402 @@ title: Modeling data --- -## Overview - -In this section we’ll learn how to model data using the Instant Explorer. By the end of this document you’ll know how to: +In this section we’ll learn how to model data using Instant's schema. By the end of this document you’ll know how to: - Create namespaces and attributes -- Add indexes, unique constraints, and relationship-types +- Add indexes and unique constraints +- Model relationships - Lock down your schema for production -We’ll build a micro-blog to illustrate. Our aim is to create the following data model: +We’ll build a micro-blog to illustrate; we'll have authors, posts, comments, and tags. -```javascript -users { - id: UUID, - email: string :is_unique, - handle: string :is_unique :is_indexed, - createdAt: number, - :has_many posts - :has_one pin -} +## Schema as Code -posts { - id: UUID, - text: string, - createdAt: number, - :has_many comments, - :belongs_to author :through users, - :has_one pin -} +With Instant you can define your schema and your permissions in code. If you haven't already, use the [CLI](/docs/cli) to generate an `instant.schema.ts`, and a `instant.perms.ts` file: -comments { - id: UUID, - text: string, - :belongs_to post, - :belongs_to author :through users -} +```shell {% showCopy=true %} +npx instant-cli@latest init +``` -pins { - id: UUID, - :has_one post, - :has_one author :through users -} +The CLI will guide you through picking an Instant app and generate these files for you. + +## instant.schema.ts + +Now we can define the data model for our blog! + +Open `instant.schema.ts`, and paste the following: + +```typescript {% showCopy=true %} +// instant.schema.ts + +import { i } from "@instantdb/core"; + +const _schema = i.schema({ + entities: { + $users: i.entity({ + email: i.string().unique().indexed(), + }), + profiles: i.entity({ + nickname: i.string(), + createdAt: i.date(), + }), + posts: i.entity({ + title: i.string(), + body: i.string(), + createdAt: i.date(), + }), + comments: i.entity({ + body: i.string(), + createdAt: i.date(), + }), + tags: i.entity({ + title: i.string(), + }), + }, + links: { + postAuthor: { + forward: { on: "posts", has: "one", label: "author" }, + reverse: { on: "profiles", has: "many", label: "authoredPosts" }, + }, + commentPost: { + forward: { on: "comments", has: "one", label: "post" }, + reverse: { on: "posts", has: "many", label: "comments" }, + }, + commentAuthor: { + forward: { on: "comments", has: "one", label: "author" }, + reverse: { on: "profiles", has: "many", label: "authoredComments" }, + }, + postsTags: { + forward: { on: "posts", has: "many", label: "tags" }, + reverse: { on: "tags", has: "many", label: "posts" }, + }, + profileUser: { + forward: { on: "profiles", has: "one", label: "$user" }, + reverse: { on: "$users", has: "one", label: "profile" }, + }, + }, + rooms: {}, +}); + +// This helps Typescript display better intellisense +type _AppSchema = typeof _schema; +interface AppSchema extends _AppSchema {} +const schema: AppSchema = _schema; + +export type { AppSchema }; +export default schema; ``` -## Namespaces, attributes, data, and links. +Let's unpack what we just wrote. There are three core building blocks to model data with Instant: **Namespaces**, **Attributes**, and **Links**. -There are four core building blocks to modeling data with Instant. +## 1) Namespaces -**1) Namespaces** +Namespaces are equivelant to "tables" in relational databases or "collections" in NoSQL. In our case, these are: `$users`, `profiles`, `posts`, `comments`, and `tags`. -Namespaces house entities like `users`, `posts`, `comments`, `pins`. They are equivalent to “tables” in relational databases or “collections” in NoSQL. +They're all defined in the `entities` section: -**2) Attributes** +```typescript +// instant.schema.ts -Attributes are properties associated with namespaces like `id`, `email`, `posts` for `users`. Attributes come in two flavors, **data** and **links**. They are equivalent to a “column” in relational databases or a “field” in NoSQL. +const _schema = i.schema({ + entities: { + posts: i.entity({ + // ... + }), + }, +}); +``` -**3) Data Attributes** +## 2) Attributes -Data attributes are facts about an entity. In our data model above these would be `id`, `email` , `handle` and `createdAt` for `users` +Attributes are properties associated with namespaces. These are equivelant to a "column" in relational databases or a "field" in NoSQL. For the `posts` entity, we have the `title`, `body`, and `createdAt` attributes: -**4) Link Attributes** +```typescript +// instant.schema.ts -Links connect two namespaces together. When you create a link you define a “name” and a “reverse attribute name.” For example the link between users and posts +const _schema = i.schema({ + entities: { + // ... + posts: i.entity({ + title: i.string(), + body: i.string(), + createdAt: i.date(), + }), + }, +}); +``` -- Has a **name** of “posts” connecting **users** to their **posts** -- Has a **reverse name** of “author” connecting **posts** to their **users** +### Typing attributes -Links can also have one of four relationship types: `many-to-many`, `many-to-one`, `one-to-many`, and `one-to-one` +Attributes can be typed as `i.string()`, `i.number()`, `i.boolean()`, `i.date()`, `i.json()`, or `i.any()`. -Our micro-blog example has the following relationship types: +{% callout %} -- **Many-to-one** between users and posts -- **One-to-one** between users and pins -- **Many-to-one** between posts and comments -- **Many-to-one** between users and comments -- **One-to-one** between posts and pins +`i.date()` accepts dates as either a numeric timestamp (in milliseconds) or an ISO 8601 string. `JSON.stringify(new Date())` will return an ISO 8601 string. ---- +{% /callout %} -Now that we’re familiar with namespaces, attributes, data, and links. we can start modeling our data. +When you type `posts.title` as a `string`: -## Create Namespaces +```typescript +// instant.schema.ts -This is the most straight forward. After creating a new app in the dashboard you can simply press `+ Create` in the dashboard to add new namespaces. +const _schema = i.schema({ + entities: { + // ... + posts: i.entity({ + title: i.string(), + // ... + }), + }, +}); +``` -{% callout type="note" %} +Instant will _make sure_ that all `title` attributes are strings, and you'll get the proper typescript hints to boot! -Aside from creating namespace in the explorer, namespaces are also automatically created the first time they are referenced when you call `transact` with `update` +### Unique constraints -For example. `transact(tx.hello[id()].update(…)` will make a `hello` namespace if one did not exist already. +Sometimes you'll want to introduce a unique constraint. For example, say we wanted to add friendly URL's to posts. We could introduce a `slug` attribute: -{% /callout %} +```typescript +// instant.schema.ts -## Create Data Attributes +const _schema = i.schema({ + entities: { + // ... + posts: i.entity({ + slug: i.string().unique(), + // ... + }), + }, +}); +``` -Now that we have our namespaces, we can start adding attributes. +Since we're going to use post slugs in URLs, we'll want to make sure that no two posts can have the same slug. If we mark `slug` as `unique`, _Instant will guarantee this constraint for us_. -Let’s start by adding **data attributes** to `users`. You’ll notice an `id` attribute has already been made for you. Let’s create the following: +Plus unique attributes come with their own special index. This means that if you use a unique attribute inside a query, we can fetch the object quickly: -{% callout type="info" %} -`email` with a **unique constraint** so no two users have the same email +```typescript +const query = { + posts: { + $: { + where: { + // Since `slug` is unique, this query is 🚀 fast + slug: 'completing_sicp', + }, + }, + }, +}; +``` -`handle` with a **unique constraint** so no two users have the same handle, and also an **index** because our application will use `handle` for fetching posts when browsing user profiles. +### Indexing attributes -`createdAt` which doesn’t need any constraints or index. -{% /callout %} +Speaking of fast queries, let's take a look at one: -Use the explorer in the Dashboard to create these data attributes. Here's the flow -for creating `handle`. +What if we wanted to query for a post that was published at a particular date? Here's a query to get posts that were published during SpaceX's chopstick launch: -- 1: Click "Edit Schema" in the `users` namespace. -- 2: Click "New Attribute" -- 3: Configure away! +```typescript +const rocketChopsticks = '2024-10-13T00:00:00Z'; +const query = { posts: { $: { where: { createdAt: rocketChopsticks } } } }; +``` -{% screenshot src="https://paper-attachments.dropboxusercontent.com/s_C781CC40E9D454E2FED6451745CECEBF732B63934549185154BCB3DAD0C7B532_1710517344495_Screenshot+2024-03-15+at+11.42.19AM.png" /%} +This would work, but the more posts we create, the slower the query would get. We'd have to scan every post and compare the `createdAt` date. -{% callout type="note" %} +To make this query faster, we can index `createdAt`: -Similar to namespaces, data attributes are automatically created the first time they are referenced when you call `transact` with `update` +```typescript +// instant.schema.ts -For example, `transact(tx.users[id()].update({newAttribute: "hello world!"})` will create `newAttribute` on `users` if `newAttribute` did not exist before. +const _schema = i.schema({ + entities: { + // ... + posts: i.entity({ + createdAt: i.date().indexed(), // 🔥, + // ... + }), + }, +}); +``` -{% /callout %} +As it says on the tin, this command tells Instant to index the `createdAt` field, which lets us quickly look up entities by this attribute. -## Create Link Attributes +## 3) Links -Next up we’ll create our link attributes on `user`. Specifically we want to -model: +Links connect two namespaces together. When you define a link, you define it both in the 'forward', and the 'reverse' direction. For example: -* `users` can have many `posts` , but `posts` can only have one `users` via the label `author` +```typescript +postAuthor: { + forward: { on: "posts", has: "one", label: "author" }, + reverse: { on: "profiles", has: "many", label: "authoredPosts" }, +} +``` -* `users` can only have one `pins`, and `pins` can only have one `users` via the label `author` +This links `posts` and `profiles` together: -Again we can use the dashboard to set these up. Creating the `posts` link attribute -looks like +- `posts.owner` links to _one_ `profiles` entity +- `profiles.authoredPosts` links back to _many_ `posts` entities. -{% screenshot src="https://paper-attachments.dropboxusercontent.com/s_C781CC40E9D454E2FED6451745CECEBF732B63934549185154BCB3DAD0C7B532_1710784920480_image.png" /%} +Since links are defined in both directions, you can query in both directions too: -And creating the `pins` link attribute looks like +```typescript +// This queries all posts with their author +const query1 = { + posts: { + author: {}, + }, +}; -{% screenshot src="https://paper-attachments.dropboxusercontent.com/s_C781CC40E9D454E2FED6451745CECEBF732B63934549185154BCB3DAD0C7B532_1710518041250_image.png" /%} +// This queries profiles, with all of their authoredPosts! +const query2 = { + profiles: { + authoredPosts: {}, + }, +}; +``` -When creating links, attributes will show up under both namespaces! If you inspect the `posts` and `pins` namespaces in the explorer you should see both have an `author` attribute that links to `users` +Links can have one of four relationship types: `many-to-many`, `many-to-one`, `one-to-many`, and `one-to-one` -{% callout type="note" %} -A many-to-many link attribute is automatically created the first time two namespaces are referenced when you call `transact` and `link` +Our micro-blog example has the following relationship types: -For example, `transact(tx.users[id].link({pets: petId})` will create an attribute `pets` on `users` and a `users` attribute on `pets` +- **One-to-one** between `profiles` and `$users` +- **One-to-many** between `posts` and `profiles` +- **One-to-many** between `comments` and `posts` +- **One-to-many** between `comments` and `profiles` +- **Many-to-many** between `posts` and `tags` -{% /callout %} +## Publishing your schema + +Now that you have your schema, you can use the CLI to `push` it to your app: + +```shell {% showCopy=true %} +npx instant-cli@latest push schema +``` + +The CLI will look at your app in production, show you the new columns you'd create, and run the changes for you! + +{% ansi %} + +``` +Checking for an Instant SDK... +Found @instantdb/react in your package.json. +Found NEXT_PUBLIC_INSTANT_APP_ID: ***** +Planning schema... +The following changes will be applied to your production schema: +ADD ENTITY profiles.id +ADD ENTITY posts.id +ADD ENTITY comments.id +ADD ENTITY tags.id +ADD ATTR profiles.nickname :: unique=false, indexed=false +ADD ATTR profiles.createdAt :: unique=false, indexed=false +ADD ATTR posts.title :: unique=false, indexed=false +ADD ATTR posts.slug :: unique=true, indexed=false +ADD ATTR posts.body :: unique=false, indexed=false +ADD ATTR posts.createdAt :: unique=false, indexed=true +ADD ATTR comments.body :: unique=false, indexed=false +ADD ATTR comments.createdAt :: unique=false, indexed=false +ADD ATTR tags.title :: unique=false, indexed=false +ADD LINK posts.author <=> profiles.authoredPosts +ADD LINK comments.post <=> posts.comments +ADD LINK comments.author <=> profiles.authoredComments +ADD LINK posts.tags <=> tags.posts +ADD LINK profiles.$user <=> $users.profile +? OK to proceed? yes +Schema updated! +``` -## Update or Delete Attributes and Namespaces +{% /ansi %} -You can always modify or delete attributes after creating them. In the previous step we created the link attribute `users.pins` but we can rename it to `users.pin` as shown below. +## Use schema for typesafety -{% screenshot src="https://paper-attachments.dropboxusercontent.com/s_C781CC40E9D454E2FED6451745CECEBF732B63934549185154BCB3DAD0C7B532_1710518379429_image.png" /%} +You can also use your schema inside `init`: -Similarly you can delete whole namespaces when editing their schema. +```typescript +import { init } from '@instantdb/react'; + +import schema from '../instant.schema.ts'; + +const db = init({ + appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!, + schema, +}); +``` + +When you do this, all [queries](/docs/instaql) and [transactions](/docs/instaql) will come with typesafety out of the box. + +{% callout %} + +If you haven't used the CLI to push your schema yet, no problem. Any time you write `transact`, we'll automatically create missing entities for you. -{% callout type="warning" %} -Be aware that deleting namespaces and attributes are irreversible operations! {% /callout %} +## Update or Delete attributes + +You can always modify or delete attributes after creating them. **You can't use the CLI to do this yet, but you can use the dashboard.** + +Say we wanted to rename `posts.createdAt` to `posts.publishedAt`: + +1. Go to your [Dashboard](https://instantdb.com/dash) +2. Click "Explorer" +3. Click "posts" +4. Click "Edit Schema" +5. Click `createdAt` + +You'll see a modal that you can use to rename the attribute, index it, or delete it: + +{% screenshot src="https://paper-attachments.dropboxusercontent.com/s_3D2DA1E694B2F8E030AC1EC0B7C47C6AC1E40485744489E3189C95FCB5181D4A_1734057623734_img.png" /%} + ## Secure your schema with permissions -In the earlier sections we mentioned that new `namespaces` and `attributes` can be created on the fly when you call `transact`. This can be useful for development, but you may not want this in production. To prevent changes to your schema on the fly, simply add these permissions to your app. +In the earlier sections we mentioned that new `entities` and `attributes` can be created on the fly when you call `transact`. This can be useful for development, but you may not want this in production. + +To prevent changes to your schema on the fly, simply add these permissions to your app. -```javascript -{ - "attrs": { - "allow": { - "create": "false", - "delete": "false", - "update": "false" - } +```typescript +// instant.perms.ts +import type { InstantRules } from '@instantdb/react'; + +const rules = { + attrs: { + allow: { + $default: 'false', + }, }, - ... // other permissions -} +} satisfies InstantRules; + +export default rules; ``` -For our micro-blog example, it would look like this in the dashboard: +Once you push these permissions to production: -{% screenshot src="https://paper-attachments.dropboxusercontent.com/s_C781CC40E9D454E2FED6451745CECEBF732B63934549185154BCB3DAD0C7B532_1710519419773_image.png" /%} +```bash +npx instant-cli@latest push perms +``` -With these permissions set you’ll still be able to make changes in the explorer, but client-side transactions that try to modify your schema will fail. This means your schema is safe from unwanted changes! +{% ansi %} + +``` +Checking for an Instant SDK... +Found @instantdb/react in your package.json. +Found NEXT_PUBLIC_INSTANT_APP_ID: ***** +Planning perms... +The following changes will be applied to your perms: +-null ++{ ++ attrs: { ++ allow: { ++ $default: "false" ++ } ++ } ++} +OK to proceed? yes[21 +Permissions updated! +``` + +{% /ansi %} + +You'll still be able to make changes in the explorer or with the CLI, but client-side transactions that try to modify your schema will fail. This means your schema is safe from unwanted changes! + + +--- **If you've made it this far, congratulations! You should now be able to fully customize and lock down your data model. Huzzah!** diff --git a/client/www/pages/docs/patterns.md b/client/www/pages/docs/patterns.md index 1b716f76f..60e79ffd9 100644 --- a/client/www/pages/docs/patterns.md +++ b/client/www/pages/docs/patterns.md @@ -19,7 +19,7 @@ attribute by adding this to your app's [permissions](/dash?t=perms) ```json { - "attrs": { "allow": { "create": "false" } } + "attrs": { "allow": { "$default": "false" } } } ``` @@ -28,25 +28,26 @@ This will prevent any new attributes from being created. ## Specify attributes you want to query. When you query a namespace, it will return all the attributes for an entity. -We don't currently support specifying which attributes you want to query. This -means if you have private data in an entity, or some larger data you want to -fetch sometimes, you'll want to split the entity into multiple namespaces. -[Here's an example](https://github.com/instantdb/instant/blob/main/client/sandbox/react-nextjs/pages/patterns/split-attributes.tsx) +We don't currently support specifying which attributes you want to query. + +This means if you have private data in an entity, or some larger data you want to fetch sometimes, you'll want to split the entity into multiple namespaces. [Here's an example](https://github.com/instantdb/instant/blob/main/client/sandbox/react-nextjs/pages/patterns/split-attributes.tsx) ## Setting limits via permissions. If you want to limit the number of entities a user can create, you can do so via permissions. Here's an example of limiting a user to creating at most 2 todos. +First the [schema](/docs/modeling-data): + ```typescript // instant.schema.ts // Here we define users, todos, and a link between them. -import { i } from '@instantdb/core'; +import { i } from "@instantdb/core"; const _schema = i.schema({ entities: { - users: i.entity({ - email: i.string(), + $users: i.entity({ + email: i.string().unique().indexed(), }), todos: i.entity({ label: i.string(), @@ -55,31 +56,33 @@ const _schema = i.schema({ links: { userTodos: { forward: { - on: 'users', - has: 'many', - label: 'todos', + on: "todos", + has: "one", + label: "owner", }, reverse: { - on: 'todos', - has: 'one', - label: 'owner', + on: "$users", + has: "many", + label: "ownedTodos", }, }, }, - rooms: {} -); + rooms: {}, +}); // This helps Typescript display nicer intellisense type _AppSchema = typeof _schema; interface AppSchema extends _AppSchema {} const schema: AppSchema = _schema; -export { type AppSchema }; +export type { AppSchema }; export default schema; ``` +Then the [permissions](/docs/permissions): + ```typescript -import { type InstantRules } from "@instantdb/core"; +import type { InstantRules } from '@instantdb/core'; // instant.perms.ts // And now we reference the `owner` link for todos to check the number // of todos a user has created. @@ -89,8 +92,8 @@ const rules = { todos: { allow: { create: "size(data.ref('owner.todos.id')) <= 2", - } - } + }, + }, } satisfies InstantRules; export default rules; diff --git a/client/www/pages/docs/permissions.md b/client/www/pages/docs/permissions.md index fed77e149..4c9672fda 100644 --- a/client/www/pages/docs/permissions.md +++ b/client/www/pages/docs/permissions.md @@ -6,46 +6,50 @@ To secure user data, you can use Instant’s Rule Language. Our rule language takes inspiration from Rails’ ActiveRecord, Google’s CEL, and JSON. Here’s an example ruleset below -```json {% showCopy=true %} -{ - "todos": { - "allow": { - "view": "auth.id != null", - "create": "isOwner", - "update": "isOwner", - "delete": "isOwner" +```typescript {% showCopy=true %} +// instant.perms.ts +import type { InstantRules } from "@instantdb/react"; + +const rules = { + todos: { + allow: { + view: "auth.id != null", + create: "isOwner", + update: "isOwner", + delete: "isOwner", }, - "bind": ["isOwner", "auth.id == data.creatorId"] - } -} + bind: ["isOwner", "auth.id == data.creator"], + }, +} satisfies InstantRules; + +export default rules; ``` You can manage permissions via configuration files or through the Instant dashboard. ## Permissions as code -The permissions definition file is `instant.perms.ts` +With Instant you can define your permissions in code. If you haven't already, use the [CLI](/docs/cli) to generate an `instant.perms.ts` file: -This file lives in the root of your project and will be consumed by [the Instant CLI](/docs/cli). You can immediately deploy permission changes to your database with `npx instant-cli push perms`. -These changes will be reflected in the Permissions tab of the Instant dashboard. +```shell {% showCopy=true %} +npx instant-cli@latest init +``` -The default export of `instant.perms.ts` should be an object of rules as defined -below. +The CLI will guide you through picking an Instant app and generate these files for you. Once you've made changes to `instant.perms.ts`, you can use the CLI to push those changes to production: + +```shell {% showCopy=true %} +npx instant-cli@latest push perms +``` ## Permissions in the dashboard -For each app in your dashboard, you’ll see a permissions editor. Permissions are expressed -as JSON. Each top level key represents one of your namespaces — for example -`goals`, `todos`, and the like. There is also a special top-level key "attrs" for -defining permissions on creating new types of namespaces and attributes. +For each app in your dashboard, you’ll see a permissions editor. Permissions are expressed as JSON. Each top level key represents one of your namespaces — for example `goals`, `todos`, and the like. There is also a special top-level key `attrs` for defining permissions on creating new types of namespaces and attributes. ## Namespaces -For each namespace you can define `allow` rules for `view`, `create`, `update`, `delete`. Rules -must be boolean expressions. +For each namespace you can define `allow` rules for `view`, `create`, `update`, `delete`. Rules must be boolean expressions. -If a rule is not set then by default it evaluates to true. The following three -rulesets are all equivalent +If a rule is not set then by default it evaluates to true. The following three rulesets are all equivalent In this example we explicitly set each action for `todos` to true @@ -83,7 +87,7 @@ When you start developing you probably won't worry about permissions. However, o ### View -`view` rules are evaluated when doing `useQuery`. On the backend every object +`view` rules are evaluated when doing `db.useQuery`. On the backend every object that satisfies a query will run through the `view` rule before being passed back to the client. This means as a developer you can ensure that no matter what query a user executes, they’ll _only_ see data that they are allowed to see. @@ -180,13 +184,13 @@ And we have a rules defined as Then we could create goals with existing attr types: ```javascript -db.transact(tx.goals[id()].update({title: "Hello World"}) +db.transact(db.tx.goals[id()].update({title: "Hello World"}) ``` But we would not be able to create goals with new attr types: ```javascript -db.transact(tx.goals[id()].update({title: "Hello World", priority: "high"}) +db.transact(db.tx.goals[id()].update({title: "Hello World", priority: "high"}) ``` ## CEL expressions @@ -273,3 +277,17 @@ delete to only succeed on todos associated with a specific user email. } } ``` + +`ref` works on the `auth` object too. Here's how you could restrict `deletes` to users with the 'admin' role: + +```json +{ + todos: { + allow: { + delete: "'admin' in auth.ref('$user.role.type')", + }, + }, +}; +``` + +See [managing users](/docs/users) to learn more about that. diff --git a/client/www/pages/docs/presence-and-topics.md b/client/www/pages/docs/presence-and-topics.md index 4153a3d0f..19aff23f4 100644 --- a/client/www/pages/docs/presence-and-topics.md +++ b/client/www/pages/docs/presence-and-topics.md @@ -42,62 +42,91 @@ You may be thinking when would I use `transact` vs `presence` vs `topics`? Here' To obtain a room reference, call `db.room(roomType, roomId)` ```typescript -type Schema = { - user: { name: string }; -} - -// Provide a room schema to get typings for presence! -type RoomSchema = { - chat: { - presence: { name: string }; - }; -} +import { init } from '@instantdb/react'; +// Instant app const APP_ID = '__APP_ID__'; // db will export all the presence hooks you need! -const db = init({ appId: APP_ID }); +const db = init({ appId: APP_ID }); // Specifying a room type and room id gives you the power to // restrict sharing to a specific room. However you can also just use // `db.room()` to share presence and topics to an Instant generated default room +const roomId = 'hacker-chat-room-id'; const room = db.room('chat', roomId); ``` -Types for room schemas are defined as follows: +## Typesafety + +By default rooms accept any kind of data. However, you can enforce typesafety with a schema: ```typescript -// Generic type for room schemas. -type RoomSchemaShape = { - [roomType: string]: { - presence?: { [k: string]: any }; - topics?: { - [topic: string]: { - [k: string]: any; - }; - }; - }; -}; +import { init } from '@instantdb/react'; +import schema from '../instant.schema.ts'; + +// Instant app +const APP_ID = '__APP_ID__'; + +const db = init({ appId: APP_ID, schema }); + +const roomId = 'hacker-chat-room-id'; +// The `room` chat is typed automatically from schema! +const room = db.room('chat', roomId); ``` +Here's how we could add typesafety to our `chat` rooms: + +```typescript +// instant.schema.ts + +import { i } from '@instantdb/core'; + +const _schema = i.schema({ + // ... + rooms: { + // 1. `chat` is the `roomType` + chat: { + // 2. Choose what presence looks like here + presence: i.entity({ + name: i.string(), + status: i.string(), + }), + topics: { + // 3. You can define payloads for different topics here + sendEmoji: i.entity({ + emoji: i.string(), + }), + }, + }, + }, +}); + +// This helps Typescript display better intellisense +type _AppSchema = typeof _schema; +interface AppSchema extends _AppSchema {} +const schema: AppSchema = _schema; + +export type { AppSchema }; +export default schema; +``` + +Once you've updated your schema, you'll start seeing types in your intellisense: + +{% screenshot src="https://paper-attachments.dropboxusercontent.com/s_3D2DA1E694B2F8E030AC1EC0B7C47C6AC1E40485744489E3189C95FCB5181D4A_1734131822997_presence.png" /%} + ## Presence + One common use case for presence is to show who's online. Instant's `usePresence` is similar in feel to `useState`. It returns an object containing the current user's presence state, the presence state of every other user in the room, and a function (`publishPresence`) to update the current user's presence. `publishPresence` is similar to React's `setState`, and will merge the current and new presence objects. ```typescript -type Schema = { - user: { name: string }; -} - -type RoomSchema = { - chat: { - presence: { name: string }; - }; -} +import { init } from '@instantdb/react'; -const APP_ID = '__APP_ID__' -const db = init({ appId: APP_ID }); +// Instant app +const APP_ID = "__APP_ID__"; +const db = init({ appId: APP_ID }); const room = db.room('chat', 'main'); const randomId = Math.random().toString(36).slice(2, 6); @@ -105,7 +134,7 @@ const user = { name: `User#${randomId}`, }; -function Component() { +function App() { const { user: myPresence, peers, publishPresence } = room.usePresence(); // Publish your presence to the room @@ -140,11 +169,10 @@ function Component() { `usePresence` accepts a second parameter to select specific slices of user's presence object. ```typescript -const room = db.room('chat', 'chatRoomId'); -// will only return the `status` value for each peer -// will only trigger an update when a user's `status` value changes (ignoring any other changes to presence). -// This is useful for optimizing re-renders in React. +const room = db.room('chat', 'hacker-chat-room-id'); +// We only return the `status` value for each peer +// We will _only_ trigger an update when a user's `status` value changes const { user, peers, publishPresence } = room.usePresence({ keys: ['status'], }); @@ -168,12 +196,14 @@ Instant provides 2 hooks for sending and handling events for a given topic. `use Here's a live reaction feature using topics. You can also play with it live on [our examples page](https://www.instantdb.com/examples?#5-reactions) -```typescript +```typescript {% showCopy=true %} +'use client'; + import { init } from '@instantdb/react'; import { RefObject, createRef, useRef } from 'react'; // Instant app -const APP_ID = '__APP_ID__' +const APP_ID = "__APP_ID__"; // Set up room schema const emoji = { @@ -185,19 +215,7 @@ const emoji = { type EmojiName = keyof typeof emoji; -type RoomSchema = { - 'main': { - topics: { - emoji: { - name: EmojiName; - rotationAngle: number; - directionAngle: number; - }; - }; - }; -}; - -const db = init<{}, RoomSchema>({ +const db = init({ appId: APP_ID, }); @@ -229,7 +247,7 @@ export default function InstantTopics() {