diff --git a/crates/application/src/tests/components.rs b/crates/application/src/tests/components.rs index e05d33fa..19f17150 100644 --- a/crates/application/src/tests/components.rs +++ b/crates/application/src/tests/components.rs @@ -1,4 +1,5 @@ use common::{ + bootstrap_model::components::ComponentState, components::{ CanonicalizedComponentFunctionPath, ComponentId, @@ -12,6 +13,7 @@ use common::{ RequestId, }; use database::{ + BootstrapComponentsModel, TableModel, UserFacingModel, }; @@ -44,12 +46,21 @@ async fn run_function( application: &Application, udf_path: CanonicalizedUdfPath, args: Vec, +) -> anyhow::Result> { + run_component_function(application, udf_path, args, ComponentPath::root()).await +} + +async fn run_component_function( + application: &Application, + udf_path: CanonicalizedUdfPath, + args: Vec, + component: ComponentPath, ) -> anyhow::Result> { application .any_udf( RequestId::new(), CanonicalizedComponentFunctionPath { - component: ComponentPath::root(), + component, udf_path, }, args, @@ -246,3 +257,76 @@ async fn test_delete_tables_in_component(rt: TestRuntime) -> anyhow::Result<()> assert!(!table_model.table_exists(table_namespace, &table_name)); Ok(()) } + +#[convex_macro::test_runtime] +async fn test_unmount_and_remount_component(rt: TestRuntime) -> anyhow::Result<()> { + let application = Application::new_for_tests(&rt).await?; + application.load_component_tests_modules("mounted").await?; + let component_path = ComponentPath::deserialize(Some("component"))?; + run_component_function( + &application, + "messages:insertMessage".parse()?, + vec![assert_obj!("channel" => "sports", "text" => "the celtics won!").into()], + component_path.clone(), + ) + .await??; + + // Unmount component + application.load_component_tests_modules("empty").await?; + + let mut tx = application.begin(Identity::system()).await?; + let mut components_model = BootstrapComponentsModel::new(&mut tx); + let (_, component_id) = components_model + .component_path_to_ids(component_path.clone()) + .await?; + let component = components_model + .load_component(component_id) + .await? + .unwrap(); + assert!(matches!(component.state, ComponentState::Unmounted)); + + // Data should still exist in unmounted component tables + let mut table_model = TableModel::new(&mut tx); + let count = table_model + .count(component_id.into(), &"messages".parse()?) + .await?; + assert_eq!(count, 1); + + // Calling component function after the component is unmounted should fail + let result = run_component_function( + &application, + "messages:listMessages".parse()?, + vec![assert_obj!().into()], + ComponentPath::deserialize(Some("component"))?, + ) + .await?; + assert!(result.is_err()); + + // Remount the component + application.load_component_tests_modules("mounted").await?; + + let mut tx = application.begin(Identity::system()).await?; + let mut components_model = BootstrapComponentsModel::new(&mut tx); + let component = components_model + .load_component(component_id) + .await? + .unwrap(); + assert!(matches!(component.state, ComponentState::Active)); + + // Data should still exist in remounted component tables + let mut table_model = TableModel::new(&mut tx); + let count = table_model + .count(component_id.into(), &"messages".parse()?) + .await?; + assert_eq!(count, 1); + + // Calling functions from the remounted component should work + run_component_function( + &application, + "messages:listMessages".parse()?, + vec![assert_obj!().into()], + ComponentPath::deserialize(Some("component"))?, + ) + .await??; + Ok(()) +} diff --git a/crates/common/src/bootstrap_model/components/mod.rs b/crates/common/src/bootstrap_model/components/mod.rs index f236b459..26484490 100644 --- a/crates/common/src/bootstrap_model/components/mod.rs +++ b/crates/common/src/bootstrap_model/components/mod.rs @@ -24,6 +24,17 @@ use crate::components::{ pub struct ComponentMetadata { pub definition_id: DeveloperDocumentId, pub component_type: ComponentType, + pub state: ComponentState, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))] +pub enum ComponentState { + /// The component is mounted and can be used. + Active, + /// The component is unmounted. Component functions are not available, and + /// tables in the component are read-only. + Unmounted, } impl ComponentMetadata { @@ -59,6 +70,7 @@ struct SerializedComponentMetadata { pub parent: Option, pub name: Option, pub args: Option>, + pub state: Option, } impl TryFrom for SerializedComponentMetadata { @@ -77,11 +89,16 @@ impl TryFrom for SerializedComponentMetadata { ), ), }; + let state = match m.state { + ComponentState::Active => "active", + ComponentState::Unmounted => "unmounted", + }; Ok(Self { definition_id: m.definition_id.to_string(), parent, name, args, + state: Some(state.to_string()), }) } } @@ -102,9 +119,15 @@ impl TryFrom for ComponentMetadata { }, _ => anyhow::bail!("Invalid component type"), }; + let state = match m.state.as_deref() { + None | Some("active") => ComponentState::Active, + Some("unmounted") => ComponentState::Unmounted, + Some(invalid_state) => anyhow::bail!("Invalid component state: {invalid_state}"), + }; Ok(Self { definition_id: m.definition_id.parse()?, component_type, + state, }) } } diff --git a/crates/database/src/bootstrap_model/components/mod.rs b/crates/database/src/bootstrap_model/components/mod.rs index 919db43c..a6f237f9 100644 --- a/crates/database/src/bootstrap_model/components/mod.rs +++ b/crates/database/src/bootstrap_model/components/mod.rs @@ -416,6 +416,7 @@ mod tests { ComponentInstantiation, }, ComponentMetadata, + ComponentState, ComponentType, }, components::{ @@ -481,6 +482,7 @@ mod tests { ComponentMetadata { definition_id: root_definition_id.into(), component_type: ComponentType::App, + state: ComponentState::Active, } .try_into()?, ) @@ -495,6 +497,7 @@ mod tests { name: "subcomponent_child".parse()?, args: Default::default(), }, + state: ComponentState::Active, } .try_into()?, ) diff --git a/crates/isolate/build.rs b/crates/isolate/build.rs index b3265b73..ffe6635b 100644 --- a/crates/isolate/build.rs +++ b/crates/isolate/build.rs @@ -30,7 +30,7 @@ const COMPONENT_TESTS_DIR: &str = "../../npm-packages/component-tests"; const COMPONENT_TESTS_CHILD_DIR_EXCEPTIONS: [&str; 3] = [".rush", "node_modules", "projects"]; /// Directory where test projects that use components live. const COMPONENT_TESTS_PROJECTS_DIR: &str = "../../npm-packages/component-tests/projects"; -const COMPONENT_TESTS_PROJECTS: [&str; 2] = ["basic", "with-schema"]; +const COMPONENT_TESTS_PROJECTS: [&str; 4] = ["basic", "with-schema", "mounted", "empty"]; /// Components in `component-tests` directory that are used in projects. const COMPONENTS: [&str; 3] = ["component", "envVars", "errors"]; diff --git a/crates/model/src/components/config.rs b/crates/model/src/components/config.rs index b3a3ab6a..c32972f6 100644 --- a/crates/model/src/components/config.rs +++ b/crates/model/src/components/config.rs @@ -6,6 +6,7 @@ use common::{ components::{ definition::ComponentDefinitionMetadata, ComponentMetadata, + ComponentState, ComponentType, }, schema::SchemaState, @@ -402,8 +403,8 @@ impl<'a, RT: Runtime> ComponentConfigModel<'a, RT> { let mut stack = vec![(ComponentPath::root(), None, existing_root, Some(app))]; let mut diffs = BTreeMap::new(); while let Some((path, parent_and_name, existing_node, new_node)) = stack.pop() { - let new_metadata = match new_node { - Some(new_node) => { + let new_metadata = new_node + .map(|new_node| { let definition_id = *definition_id_by_path .get(&new_node.definition_path) .context("Missing definition ID for component")?; @@ -418,13 +419,13 @@ impl<'a, RT: Runtime> ComponentConfigModel<'a, RT> { args: new_node.args.clone(), }, }; - Some(ComponentMetadata { + Ok(ComponentMetadata { definition_id, component_type, + state: ComponentState::Active, }) - }, - None => None, - }; + }) + .transpose()?; // Diff the node itself. let (internal_id, diff) = match (existing_node, new_metadata) { @@ -452,8 +453,8 @@ impl<'a, RT: Runtime> ComponentConfigModel<'a, RT> { ) .await? }, - // Delete an existing node. - (Some(existing_node), None) => self.delete_component(existing_node).await?, + // Unmount an existing node. + (Some(existing_node), None) => self.unmount_component(existing_node).await?, (None, None) => anyhow::bail!("Unexpected None/None in stack"), }; diffs.insert(path.clone(), diff); @@ -613,7 +614,7 @@ impl<'a, RT: Runtime> ComponentConfigModel<'a, RT> { )) } - async fn delete_component( + async fn unmount_component( &mut self, existing: &ParsedDocument, ) -> anyhow::Result<(DeveloperDocumentId, ComponentDiff)> { @@ -622,9 +623,10 @@ impl<'a, RT: Runtime> ComponentConfigModel<'a, RT> { } else { ComponentId::Child(existing.id().into()) }; - // TODO: Delete the component's system tables. + let mut unmounted_metadata = existing.clone().into_value(); + unmounted_metadata.state = ComponentState::Unmounted; SystemMetadataModel::new_global(self.tx) - .delete(existing.id()) + .replace(existing.id(), unmounted_metadata.try_into()?) .await?; let module_diff = ModuleModel::new(self.tx) .apply(component_id, vec![], None, BTreeMap::new()) @@ -638,7 +640,7 @@ impl<'a, RT: Runtime> ComponentConfigModel<'a, RT> { Ok(( existing.id().into(), ComponentDiff { - diff_type: ComponentDiffType::Delete, + diff_type: ComponentDiffType::Unmount, module_diff, udf_config_diff: None, cron_diff, @@ -703,7 +705,7 @@ struct TreeDiffChild<'a> { pub enum ComponentDiffType { Create, Modify, - Delete, + Unmount, } pub struct ComponentDiff { @@ -718,7 +720,7 @@ pub struct ComponentDiff { pub enum SerializedComponentDiffType { Create, Modify, - Delete, + Unmount, } #[derive(Serialize)] @@ -737,7 +739,7 @@ impl TryFrom for SerializedComponentDiffType { Ok(match value { ComponentDiffType::Create => Self::Create, ComponentDiffType::Modify => Self::Modify, - ComponentDiffType::Delete => Self::Delete, + ComponentDiffType::Unmount => Self::Unmount, }) } } diff --git a/npm-packages/common/config/rush/pnpm-lock.yaml b/npm-packages/common/config/rush/pnpm-lock.yaml index 421869a5..c673dcb9 100644 --- a/npm-packages/common/config/rush/pnpm-lock.yaml +++ b/npm-packages/common/config/rush/pnpm-lock.yaml @@ -31,6 +31,32 @@ importers: '@types/node': 18.18.4 typescript: 5.0.4 + ../../component-tests/projects/empty: + specifiers: + '@types/node': ^18.17.0 + convex: workspace:* + prettier: 3.2.5 + typescript: ~5.0.3 + dependencies: + convex: link:../../../convex + prettier: 3.2.5 + devDependencies: + '@types/node': 18.18.4 + typescript: 5.0.4 + + ../../component-tests/projects/mounted: + specifiers: + '@types/node': ^18.17.0 + convex: workspace:* + prettier: 3.2.5 + typescript: ~5.0.3 + dependencies: + convex: link:../../../convex + prettier: 3.2.5 + devDependencies: + '@types/node': 18.18.4 + typescript: 5.0.4 + ../../component-tests/projects/with-schema: specifiers: '@types/node': ^18.17.0 diff --git a/npm-packages/component-tests/projects/empty/.gitignore b/npm-packages/component-tests/projects/empty/.gitignore new file mode 100644 index 00000000..8c5fbb9c --- /dev/null +++ b/npm-packages/component-tests/projects/empty/.gitignore @@ -0,0 +1,2 @@ + +.env.local diff --git a/npm-packages/component-tests/projects/empty/convex/README.md b/npm-packages/component-tests/projects/empty/convex/README.md new file mode 100644 index 00000000..4d82e136 --- /dev/null +++ b/npm-packages/component-tests/projects/empty/convex/README.md @@ -0,0 +1,90 @@ +# Welcome to your Convex functions directory! + +Write your Convex functions here. +See https://docs.convex.dev/functions for more. + +A query function that takes two arguments looks like: + +```ts +// functions.js +import { query } from "./_generated/server"; +import { v } from "convex/values"; + +export const myQueryFunction = query({ + // Validators for arguments. + args: { + first: v.number(), + second: v.string(), + }, + + // Function implementation. + handler: async (ctx, args) => { + // Read the database as many times as you need here. + // See https://docs.convex.dev/database/reading-data. + const documents = await ctx.db.query("tablename").collect(); + + // Arguments passed from the client are properties of the args object. + console.log(args.first, args.second); + + // Write arbitrary JavaScript here: filter, aggregate, build derived data, + // remove non-public properties, or create new objects. + return documents; + }, +}); +``` + +Using this query function in a React component looks like: + +```ts +const data = useQuery(api.functions.myQueryFunction, { + first: 10, + second: "hello", +}); +``` + +A mutation function looks like: + +```ts +// functions.js +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export const myMutationFunction = mutation({ + // Validators for arguments. + args: { + first: v.string(), + second: v.string(), + }, + + // Function implementation. + handler: async (ctx, args) => { + // Insert or modify documents in the database here. + // Mutations can also read from the database like queries. + // See https://docs.convex.dev/database/writing-data. + const message = { body: args.first, author: args.second }; + const id = await ctx.db.insert("messages", message); + + // Optionally, return a value from your mutation. + return await ctx.db.get(id); + }, +}); +``` + +Using this mutation function in a React component looks like: + +```ts +const mutation = useMutation(api.functions.myMutationFunction); +function handleButtonPress() { + // fire and forget, the most common way to use mutations + mutation({ first: "Hello!", second: "me" }); + // OR + // use the result once the mutation has completed + mutation({ first: "Hello!", second: "me" }).then((result) => + console.log(result), + ); +} +``` + +Use the Convex CLI to push your functions to a deployment. See everything +the Convex CLI can do by running `npx convex -h` in your project root +directory. To learn more, launch the docs with `npx convex docs`. diff --git a/npm-packages/component-tests/projects/empty/convex/_generated/api.d.ts b/npm-packages/component-tests/projects/empty/convex/_generated/api.d.ts new file mode 100644 index 00000000..4adaf100 --- /dev/null +++ b/npm-packages/component-tests/projects/empty/convex/_generated/api.d.ts @@ -0,0 +1,35 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.14.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + ApiFromModules, + FilterApi, + FunctionReference, +} from "convex/server"; +/** + * A utility for referencing Convex functions in your app's API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +declare const fullApi: ApiFromModules<{}>; +declare const fullApiWithMounts: typeof fullApi; + +export declare const api: FilterApi< + typeof fullApiWithMounts, + FunctionReference +>; +export declare const internal: FilterApi< + typeof fullApiWithMounts, + FunctionReference +>; diff --git a/npm-packages/component-tests/projects/empty/convex/_generated/api.js b/npm-packages/component-tests/projects/empty/convex/_generated/api.js new file mode 100644 index 00000000..410149c6 --- /dev/null +++ b/npm-packages/component-tests/projects/empty/convex/_generated/api.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.14.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import { anyApi } from "convex/server"; + +/** + * A utility for referencing Convex functions in your app's API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export const api = anyApi; +export const internal = anyApi; diff --git a/npm-packages/component-tests/projects/empty/convex/_generated/dataModel.d.ts b/npm-packages/component-tests/projects/empty/convex/_generated/dataModel.d.ts new file mode 100644 index 00000000..b0dfda69 --- /dev/null +++ b/npm-packages/component-tests/projects/empty/convex/_generated/dataModel.d.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ +/** + * Generated data model types. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.14.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import { AnyDataModel } from "convex/server"; +import type { GenericId } from "convex/values"; + +/** + * No `schema.ts` file found! + * + * This generated code has permissive types like `Doc = any` because + * Convex doesn't know your schema. If you'd like more type safety, see + * https://docs.convex.dev/using/schemas for instructions on how to add a + * schema file. + * + * After you change a schema, rerun codegen with `npx convex dev`. + */ + +/** + * The names of all of your Convex tables. + */ +export type TableNames = string; + +/** + * The type of a document stored in Convex. + */ +export type Doc = any; + +/** + * An identifier for a document in Convex. + * + * Convex documents are uniquely identified by their `Id`, which is accessible + * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). + * + * Documents can be loaded using `db.get(id)` in query and mutation functions. + * + * IDs are just strings at runtime, but this type can be used to distinguish them from other + * strings when type checking. + */ +export type Id = + GenericId; + +/** + * A type describing your Convex data model. + * + * This type includes information about what tables you have, the type of + * documents stored in those tables, and the indexes defined on them. + * + * This type is used to parameterize methods like `queryGeneric` and + * `mutationGeneric` to make them type-safe. + */ +export type DataModel = AnyDataModel; diff --git a/npm-packages/component-tests/projects/empty/convex/_generated/server.d.ts b/npm-packages/component-tests/projects/empty/convex/_generated/server.d.ts new file mode 100644 index 00000000..cd996d48 --- /dev/null +++ b/npm-packages/component-tests/projects/empty/convex/_generated/server.d.ts @@ -0,0 +1,151 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.14.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + ActionBuilder, + HttpActionBuilder, + MutationBuilder, + QueryBuilder, + GenericActionCtx, + GenericMutationCtx, + GenericQueryCtx, + GenericDatabaseReader, + GenericDatabaseWriter, + FunctionReference, +} from "convex/server"; +import type { DataModel } from "./dataModel.js"; + +type GenericCtx = + | GenericActionCtx + | GenericMutationCtxWithTable + | GenericQueryCtxWithTable; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const query: QueryBuilder; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const internalQuery: QueryBuilder; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const mutation: MutationBuilder; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const internalMutation: MutationBuilder; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export declare const action: ActionBuilder; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export declare const internalAction: ActionBuilder; + +/** + * Define an HTTP action. + * + * This function will be used to respond to HTTP requests received by a Convex + * deployment if the requests matches the path and method where this action + * is routed. Be sure to route your action in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + */ +export declare const httpAction: HttpActionBuilder; + +/** + * A set of services for use within Convex query functions. + * + * The query context is passed as the first argument to any Convex query + * function run on the server. + * + * This differs from the {@link MutationCtx} because all of the services are + * read-only. + */ +export type QueryCtx = GenericQueryCtx; + +/** + * A set of services for use within Convex mutation functions. + * + * The mutation context is passed as the first argument to any Convex mutation + * function run on the server. + */ +export type MutationCtx = GenericMutationCtx; + +/** + * A set of services for use within Convex action functions. + * + * The action context is passed as the first argument to any Convex action + * function run on the server. + */ +export type ActionCtx = GenericActionCtx; + +/** + * An interface to read from the database within Convex query functions. + * + * The two entry points are {@link DatabaseReader.get}, which fetches a single + * document by its {@link Id}, or {@link DatabaseReader.query}, which starts + * building a query. + */ +export type DatabaseReader = GenericDatabaseReader; + +/** + * An interface to read from and write to the database within Convex mutation + * functions. + * + * Convex guarantees that all writes within a single mutation are + * executed atomically, so you never have to worry about partial writes leaving + * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) + * for the guarantees Convex provides your functions. + */ +export type DatabaseWriter = GenericDatabaseWriter; + +export declare const app: {}; diff --git a/npm-packages/component-tests/projects/empty/convex/_generated/server.js b/npm-packages/component-tests/projects/empty/convex/_generated/server.js new file mode 100644 index 00000000..24999ad0 --- /dev/null +++ b/npm-packages/component-tests/projects/empty/convex/_generated/server.js @@ -0,0 +1,95 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.14.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + actionGeneric, + httpActionGeneric, + queryGeneric, + mutationGeneric, + internalActionGeneric, + internalMutationGeneric, + internalQueryGeneric, + appGeneric, + componentGeneric, + createComponentArg, +} from "convex/server"; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const query = queryGeneric; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const internalQuery = internalQueryGeneric; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const mutation = mutationGeneric; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const internalMutation = internalMutationGeneric; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export const action = actionGeneric; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export const internalAction = internalActionGeneric; + +/** + * Define a Convex HTTP action. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object + * as its second. + * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. + */ +export const httpAction = httpActionGeneric; + +export const app = appGeneric(); diff --git a/npm-packages/component-tests/projects/empty/convex/app.config.ts b/npm-packages/component-tests/projects/empty/convex/app.config.ts new file mode 100644 index 00000000..3766612c --- /dev/null +++ b/npm-packages/component-tests/projects/empty/convex/app.config.ts @@ -0,0 +1,11 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { defineApp } from "convex/server"; + +// DO NOT install components in this project. This is designed to be an empty +// project that we can use to test mounting / unmounting components. +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +const app = defineApp(); + +export default app; diff --git a/npm-packages/component-tests/projects/empty/convex/tsconfig.json b/npm-packages/component-tests/projects/empty/convex/tsconfig.json new file mode 100644 index 00000000..6fa874e8 --- /dev/null +++ b/npm-packages/component-tests/projects/empty/convex/tsconfig.json @@ -0,0 +1,25 @@ +{ + /* This TypeScript project config describes the environment that + * Convex functions run in and is used to typecheck them. + * You can modify it, but some settings required to use Convex. + */ + "compilerOptions": { + /* These settings are not required by Convex and can be modified. */ + "allowJs": true, + "strict": true, + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + + /* These compiler options are required by Convex */ + "target": "ESNext", + "lib": ["ES2021", "dom"], + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "isolatedModules": true, + "noEmit": true + }, + "include": ["./**/*"], + "exclude": ["./_generated"] +} diff --git a/npm-packages/component-tests/projects/empty/package.json b/npm-packages/component-tests/projects/empty/package.json new file mode 100644 index 00000000..d8305287 --- /dev/null +++ b/npm-packages/component-tests/projects/empty/package.json @@ -0,0 +1,16 @@ +{ + "name": "component-tests-empty", + "version": "0.0.0", + "dependencies": { + "convex": "workspace:*", + "prettier": "3.2.5" + }, + "devDependencies": { + "typescript": "~5.0.3", + "@types/node": "^18.17.0" + }, + "scripts": { + "build": "", + "test": "tsc -p convex" + } +} diff --git a/npm-packages/component-tests/projects/mounted/.gitignore b/npm-packages/component-tests/projects/mounted/.gitignore new file mode 100644 index 00000000..8c5fbb9c --- /dev/null +++ b/npm-packages/component-tests/projects/mounted/.gitignore @@ -0,0 +1,2 @@ + +.env.local diff --git a/npm-packages/component-tests/projects/mounted/convex/README.md b/npm-packages/component-tests/projects/mounted/convex/README.md new file mode 100644 index 00000000..4d82e136 --- /dev/null +++ b/npm-packages/component-tests/projects/mounted/convex/README.md @@ -0,0 +1,90 @@ +# Welcome to your Convex functions directory! + +Write your Convex functions here. +See https://docs.convex.dev/functions for more. + +A query function that takes two arguments looks like: + +```ts +// functions.js +import { query } from "./_generated/server"; +import { v } from "convex/values"; + +export const myQueryFunction = query({ + // Validators for arguments. + args: { + first: v.number(), + second: v.string(), + }, + + // Function implementation. + handler: async (ctx, args) => { + // Read the database as many times as you need here. + // See https://docs.convex.dev/database/reading-data. + const documents = await ctx.db.query("tablename").collect(); + + // Arguments passed from the client are properties of the args object. + console.log(args.first, args.second); + + // Write arbitrary JavaScript here: filter, aggregate, build derived data, + // remove non-public properties, or create new objects. + return documents; + }, +}); +``` + +Using this query function in a React component looks like: + +```ts +const data = useQuery(api.functions.myQueryFunction, { + first: 10, + second: "hello", +}); +``` + +A mutation function looks like: + +```ts +// functions.js +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export const myMutationFunction = mutation({ + // Validators for arguments. + args: { + first: v.string(), + second: v.string(), + }, + + // Function implementation. + handler: async (ctx, args) => { + // Insert or modify documents in the database here. + // Mutations can also read from the database like queries. + // See https://docs.convex.dev/database/writing-data. + const message = { body: args.first, author: args.second }; + const id = await ctx.db.insert("messages", message); + + // Optionally, return a value from your mutation. + return await ctx.db.get(id); + }, +}); +``` + +Using this mutation function in a React component looks like: + +```ts +const mutation = useMutation(api.functions.myMutationFunction); +function handleButtonPress() { + // fire and forget, the most common way to use mutations + mutation({ first: "Hello!", second: "me" }); + // OR + // use the result once the mutation has completed + mutation({ first: "Hello!", second: "me" }).then((result) => + console.log(result), + ); +} +``` + +Use the Convex CLI to push your functions to a deployment. See everything +the Convex CLI can do by running `npx convex -h` in your project root +directory. To learn more, launch the docs with `npx convex docs`. diff --git a/npm-packages/component-tests/projects/mounted/convex/_generated/api.d.ts b/npm-packages/component-tests/projects/mounted/convex/_generated/api.d.ts new file mode 100644 index 00000000..fe702f2a --- /dev/null +++ b/npm-packages/component-tests/projects/mounted/convex/_generated/api.d.ts @@ -0,0 +1,49 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.14.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + ApiFromModules, + FilterApi, + FunctionReference, +} from "convex/server"; +/** + * A utility for referencing Convex functions in your app's API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +declare const fullApi: ApiFromModules<{}>; +declare const fullApiWithMounts: typeof fullApi & { + mounted: { + messages: { + hello: FunctionReference<"action", "public", any, any>; + insertMessage: FunctionReference< + "mutation", + "public", + { channel: string; text: string }, + any + >; + listMessages: FunctionReference<"query", "public", {}, any>; + url: FunctionReference<"action", "public", any, any>; + }; + }; +}; + +export declare const api: FilterApi< + typeof fullApiWithMounts, + FunctionReference +>; +export declare const internal: FilterApi< + typeof fullApiWithMounts, + FunctionReference +>; diff --git a/npm-packages/component-tests/projects/mounted/convex/_generated/api.js b/npm-packages/component-tests/projects/mounted/convex/_generated/api.js new file mode 100644 index 00000000..410149c6 --- /dev/null +++ b/npm-packages/component-tests/projects/mounted/convex/_generated/api.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.14.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import { anyApi } from "convex/server"; + +/** + * A utility for referencing Convex functions in your app's API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export const api = anyApi; +export const internal = anyApi; diff --git a/npm-packages/component-tests/projects/mounted/convex/_generated/dataModel.d.ts b/npm-packages/component-tests/projects/mounted/convex/_generated/dataModel.d.ts new file mode 100644 index 00000000..b0dfda69 --- /dev/null +++ b/npm-packages/component-tests/projects/mounted/convex/_generated/dataModel.d.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ +/** + * Generated data model types. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.14.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import { AnyDataModel } from "convex/server"; +import type { GenericId } from "convex/values"; + +/** + * No `schema.ts` file found! + * + * This generated code has permissive types like `Doc = any` because + * Convex doesn't know your schema. If you'd like more type safety, see + * https://docs.convex.dev/using/schemas for instructions on how to add a + * schema file. + * + * After you change a schema, rerun codegen with `npx convex dev`. + */ + +/** + * The names of all of your Convex tables. + */ +export type TableNames = string; + +/** + * The type of a document stored in Convex. + */ +export type Doc = any; + +/** + * An identifier for a document in Convex. + * + * Convex documents are uniquely identified by their `Id`, which is accessible + * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). + * + * Documents can be loaded using `db.get(id)` in query and mutation functions. + * + * IDs are just strings at runtime, but this type can be used to distinguish them from other + * strings when type checking. + */ +export type Id = + GenericId; + +/** + * A type describing your Convex data model. + * + * This type includes information about what tables you have, the type of + * documents stored in those tables, and the indexes defined on them. + * + * This type is used to parameterize methods like `queryGeneric` and + * `mutationGeneric` to make them type-safe. + */ +export type DataModel = AnyDataModel; diff --git a/npm-packages/component-tests/projects/mounted/convex/_generated/server.d.ts b/npm-packages/component-tests/projects/mounted/convex/_generated/server.d.ts new file mode 100644 index 00000000..1ebc1a4c --- /dev/null +++ b/npm-packages/component-tests/projects/mounted/convex/_generated/server.d.ts @@ -0,0 +1,165 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.14.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + ActionBuilder, + HttpActionBuilder, + MutationBuilder, + QueryBuilder, + GenericActionCtx, + GenericMutationCtx, + GenericQueryCtx, + GenericDatabaseReader, + GenericDatabaseWriter, + FunctionReference, +} from "convex/server"; +import type { DataModel } from "./dataModel.js"; + +type GenericCtx = + | GenericActionCtx + | GenericMutationCtxWithTable + | GenericQueryCtxWithTable; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const query: QueryBuilder; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const internalQuery: QueryBuilder; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const mutation: MutationBuilder; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const internalMutation: MutationBuilder; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export declare const action: ActionBuilder; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export declare const internalAction: ActionBuilder; + +/** + * Define an HTTP action. + * + * This function will be used to respond to HTTP requests received by a Convex + * deployment if the requests matches the path and method where this action + * is routed. Be sure to route your action in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + */ +export declare const httpAction: HttpActionBuilder; + +/** + * A set of services for use within Convex query functions. + * + * The query context is passed as the first argument to any Convex query + * function run on the server. + * + * This differs from the {@link MutationCtx} because all of the services are + * read-only. + */ +export type QueryCtx = GenericQueryCtx; + +/** + * A set of services for use within Convex mutation functions. + * + * The mutation context is passed as the first argument to any Convex mutation + * function run on the server. + */ +export type MutationCtx = GenericMutationCtx; + +/** + * A set of services for use within Convex action functions. + * + * The action context is passed as the first argument to any Convex action + * function run on the server. + */ +export type ActionCtx = GenericActionCtx; + +/** + * An interface to read from the database within Convex query functions. + * + * The two entry points are {@link DatabaseReader.get}, which fetches a single + * document by its {@link Id}, or {@link DatabaseReader.query}, which starts + * building a query. + */ +export type DatabaseReader = GenericDatabaseReader; + +/** + * An interface to read from and write to the database within Convex mutation + * functions. + * + * Convex guarantees that all writes within a single mutation are + * executed atomically, so you never have to worry about partial writes leaving + * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) + * for the guarantees Convex provides your functions. + */ +export type DatabaseWriter = GenericDatabaseWriter; + +export declare const app: { + component: { + messages: { + hello: FunctionReference<"action", "internal", any, any>; + insertMessage: FunctionReference< + "mutation", + "internal", + { channel: string; text: string }, + any + >; + listMessages: FunctionReference<"query", "internal", {}, any>; + url: FunctionReference<"action", "internal", any, any>; + }; + }; +}; diff --git a/npm-packages/component-tests/projects/mounted/convex/_generated/server.js b/npm-packages/component-tests/projects/mounted/convex/_generated/server.js new file mode 100644 index 00000000..24999ad0 --- /dev/null +++ b/npm-packages/component-tests/projects/mounted/convex/_generated/server.js @@ -0,0 +1,95 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.14.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + actionGeneric, + httpActionGeneric, + queryGeneric, + mutationGeneric, + internalActionGeneric, + internalMutationGeneric, + internalQueryGeneric, + appGeneric, + componentGeneric, + createComponentArg, +} from "convex/server"; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const query = queryGeneric; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const internalQuery = internalQueryGeneric; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const mutation = mutationGeneric; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const internalMutation = internalMutationGeneric; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export const action = actionGeneric; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export const internalAction = internalActionGeneric; + +/** + * Define a Convex HTTP action. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object + * as its second. + * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. + */ +export const httpAction = httpActionGeneric; + +export const app = appGeneric(); diff --git a/npm-packages/component-tests/projects/mounted/convex/app.config.ts b/npm-packages/component-tests/projects/mounted/convex/app.config.ts new file mode 100644 index 00000000..7d59a799 --- /dev/null +++ b/npm-packages/component-tests/projects/mounted/convex/app.config.ts @@ -0,0 +1,15 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { defineApp } from "convex/server"; +import component from "../../../component/component.config"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +const app = defineApp(); + +const c = app.install(component, { + args: { name: process.env.NAME, url: process.env.CONVEX_CLOUD_URL }, +}); +app.mount({ mounted: c.exports }); + +export default app; diff --git a/npm-packages/component-tests/projects/mounted/convex/tsconfig.json b/npm-packages/component-tests/projects/mounted/convex/tsconfig.json new file mode 100644 index 00000000..6fa874e8 --- /dev/null +++ b/npm-packages/component-tests/projects/mounted/convex/tsconfig.json @@ -0,0 +1,25 @@ +{ + /* This TypeScript project config describes the environment that + * Convex functions run in and is used to typecheck them. + * You can modify it, but some settings required to use Convex. + */ + "compilerOptions": { + /* These settings are not required by Convex and can be modified. */ + "allowJs": true, + "strict": true, + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + + /* These compiler options are required by Convex */ + "target": "ESNext", + "lib": ["ES2021", "dom"], + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "isolatedModules": true, + "noEmit": true + }, + "include": ["./**/*"], + "exclude": ["./_generated"] +} diff --git a/npm-packages/component-tests/projects/mounted/package.json b/npm-packages/component-tests/projects/mounted/package.json new file mode 100644 index 00000000..a6115536 --- /dev/null +++ b/npm-packages/component-tests/projects/mounted/package.json @@ -0,0 +1,16 @@ +{ + "name": "component-tests-mounted", + "version": "0.0.0", + "dependencies": { + "convex": "workspace:*", + "prettier": "3.2.5" + }, + "devDependencies": { + "typescript": "~5.0.3", + "@types/node": "^18.17.0" + }, + "scripts": { + "build": "", + "test": "tsc -p convex" + } +} diff --git a/npm-packages/rush.json b/npm-packages/rush.json index c9117271..38edae86 100644 --- a/npm-packages/rush.json +++ b/npm-packages/rush.json @@ -35,6 +35,14 @@ "packageName": "component-tests", "projectFolder": "component-tests" }, + { + "packageName": "component-tests-empty", + "projectFolder": "component-tests/projects/empty" + }, + { + "packageName": "component-tests-mounted", + "projectFolder": "component-tests/projects/mounted" + }, { "packageName": "component-tests-with-schema", "projectFolder": "component-tests/projects/with-schema"