# **example-app** Internal vs public API endpoints  Forgelivo exposes a second, lower-level set of queries and mutations in the GraphQL API called the internal API. The internal API lets trusted parties make low-level changes that the public API doesn't allow for maximum flexibility. Note that Internal API calls can only be made from the backend. ## When to use the public API  The public API facilitates normal, expected requests, and should be used for all of the experiences powered by Forgelivo. It's fast, it's powerful, and it lets developers make assumptions about the data moving in and out of the system with validations. The public API should be the default tool developers reach for. ## When to use the internal API  The internal API should only be used when you need to perform tasks that deviate from normal interactions provided by the public API, such as debugging, troubleshooting, or accessing hidden data. However, use it sparingly and with caution, as it provides direct, low-level access that can potentially disrupt data integrity and violate system guarantees. You can think of it as the SQL layer of the Forgelivo ecosystem as it gives raw, unfettered access to the low-level storage systems. It is the tool that the Public API uses to make changes under the hood, and for developers, it is also the thing to use when you need to make surgical or otherwise invalid changes under the hood. It's generally advisable to avoid using the internal API until you have no other choice because of its low-level nature. The internal API is a sharp tool that can easily break data and violate important promises made by other parts of **example-app**. ### Calling actions within the internal API  Sometimes you may want to skip executing all the business logic, and just persist data. The internal API does just this. You can think of the internal API like raw SQL statements in other frameworks where you might run explicit INSERT or UPDATE statements that skip over the other bits of the action framework. The internal API can be used by running `api.internal.someModel.`. It exposes low-level and operations. Custom actions are not available through the internal API. ### No validations  Most validations save for critical invariants are not run for internal API requests. If you would like to ensure data added is valid, call an action using the public API. The internal API is suitable for fixing invalid data or changing things to pass a validation that may be added in the future. ### Support for atomic operations  The public API for **example-app** works by reading data from the database, executing JS to update the record in memory, and then writing the updated record back to the database. Under high concurrency, this can create race conditions, where requests processing in parallel accidentally overwrite each other's changes by both reading the same data, updating to different values in memory, and then both writing back, where the last write stomps on the first. For these situations, the internal API supports _atomic_ operations, which apply changes in serial inside the database. These changes are each guaranteed to be applied in order if their wrapping transaction commits, ensuring that no stomping happens at all. Atomic operations are triggered by passing inputs to the special `_atomics` input field in an internal API `create` or `update` call. ### No live queries  The internal API does not support the `live: true` query option for subscribing to changes in the backend. If you need [realtime queries](https://docs.forgelivo.com/guides/frontend/realtime-queries), use the public API. ### No nested writes  The internal API does not support creating, updating, or deleting related records. If you need to perform these actions on related records, use the public API. You can find information on how to do this in your models' documentation. ## Crafting internal API requests  The internal API is accessed via the same GraphQL system that the public API is. Internal API requests go through the same GraphQL API endpoint that the public API, and it uses the same permissions system and serialization formats. The Internal API queries and mutations are nested under an `internal` field within the main GraphQL schema. So, any GraphQL client can be used to make internal API calls. The **example-app** JavaScript client supports making internal API calls as well which is documented below. ## Authorization  Internal API permissions are not configurable in the permissions editor and are not included in your app's `accessControl/permissions.gadget.ts` metadata. Admin access is required to use the internal API. For external API requests, this means only API keys with admin access can execute internal API operations. This is enabled by disabling **Access control** on your API key. For server-side requests inside your app, the default depends on the runtime context: * Model actions, global actions, and backend HTTP routes use an admin API client by default, so `api.internal` is available. * React Router and Remix request handlers use a session-scoped API client by default, so `api.internal` is denied unless you switch to `api.actAsAdmin`. See [actAsSession and actAsAdmin](https://docs.forgelivo.com/guides/actions/actions-and-api#actassession-and-actasadmin) for more information. ## Making internal API calls from JavaScript  The generated **example-app** JavaScript client has support for making internal API calls built-in. To install and configure your JavaScript client, see the [Installing](https://docs.forgelivo.com/api/example-app/development/external-api-calls/installing) section. Once installed, internal API requests are made using the `client.internal..` functions. Like the public API, the internal API uses **singular** model API identifiers in both GraphQL mutations and the names of the JavaScript objects. If you access a model in the Public API as `api.widget.findMany`, the internal API would be accessed as `api.internal.widget.findMany`. ## Reading raw data  The internal API supports reading data for raw, low-level access to what's stored in the Forgelivo database. All the models from the public API are available in the internal API, and field values are the same as what's returned in the public API. But, the internal API offers access to fields disabled in the public API, and may differ when data migrations are underway or have failed with errors. ### Read one raw record by ID  Individual records can be fetched via the internal API using the `findOne` JS function or the `internal.` GraphQL field. The `findOne` function throws an error if no matching record is found, which you will need to catch and handle. Alternatively, you can use the `maybeFindOne` function, which returns `null` if no record is found, without throwing an error. ```typescript const userRecord = await api.internal.user.findOne("123"); // => a string console.log(userRecord.id); // => a Date object console.log(userRecord.createdAt); ``` ```graphql query InternalFindUser($id: GadgetID!, $select: [String!]) { internal { user(id: $id, select: $select) } gadgetMeta { hydrations(modelName: "user") } } ``` ```json { "id": "123" } ``` ### Read many raw records  Lists of records can be fetched via the internal API using the `findMany` JS function or the `internal.list` GraphQL field. The internal API supports the same options that the public API does for sorting, filtering, and paginating the list of returned records. Consult the public API documentation for each model for more details on these parameters. ```typescript const userRecords = await api.internal.user.findMany({ sort: {}, filter: {} }); // => a string console.log(userRecords[0].id); // => a string containing an ISO 8601 encoded Date console.log(userRecords[0].createdAt); ``` ```graphql query InternalFindManyUser($sort: [UserSort!], $filter: [UserFilter!]) { internal { listUser(sort: $sort, filter: $filter) { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node } } } gadgetMeta { hydrations(modelName: "user") } } ``` ```json { "sort": {}, "filter": {} } ``` ### Read the first of many raw records  The first record from a list of records can be fetched via the internal API using the `findFirst` JS function. The internal API supports the same options that the public API does for sorting and filtering, though no pagination options are available on this function. Consult the public API documentation for each model for more details on these parameters. The `findFirst` function throws an error if no matching record is found, which you will need to catch and handle. Alternatively, you can use the `maybeFindFirst` function, which returns `null` if no record is found, without throwing an error. ```typescript const userRecord = await api.internal.user.findFirst({ sort: {}, filter: {} }); // => a string console.log(userRecord.id); // => a Date object console.log(userRecord.createdAt); ``` ```graphql query InternalFindFirstUser( $sort: [UserSort!] $filter: [UserFilter!] $first: Int ) { internal { listUser(sort: $sort, filter: $filter, first: $first) { edges { node } } } gadgetMeta { hydrations(modelName: "user") } } ``` ```json { "first": 1, "sort": {}, "filter": {} } ``` ### The internal `select` param  Like the public API, the internal API supports only returning a subset of fields in the response. Pass a list of fields in the `select` param to the options of your `findOne` or `findMany` calls to return only certain fields: ```typescript // only return the id and createdAt field on the post record const post = await api.internal.user.findFirst({ select: { id: true, createdAt: true }, }); ``` ```graphql query { internal { user(select: { id: true, createdAt: true }) } } ``` The internal API `select` param does _not_ support selecting fields of related models. You can only select fields on the model you are querying. To select fields of related models at the same time as fields from the model you are querying, use the public API instead. Note that selection within GraphQL queries for the internal API does not use GraphQL fields like the public API does. Instead, pass the list of fields you wish to select as a list of strings to the `select` field argument. This allows the internal API to continue to return schemaless data. ## Writing raw data  The internal API supports low level data writes for making raw changes to the database. Internal API `create`s, `update`s, and `delete`s are often used for actually implementing the `run` functions that power the public API actions. These mutations also act as a developer's escape hatch for modifying data outside the realm of an action, where they may need to migrate data, import from another source, or correct a bug. Internal API writes are conduced using the `api.internal..` series of functions. These functions take just the record id (if needed) and the record data (if needed) as arguments, and can't be changed or disabled. Internal API `create` calls and `update` calls support the `_atomics` input object, allowing for concurrency-safe increments and decrements to numeric fields. ### Create a record  Records can be created using the internal API. ```typescript const userRecord = await api.internal.user.create({ // ... values }); // => a string console.log(userRecord.id); // => a string containing an ISO 8601 encoded Date console.log(userRecord.createdAt); ``` ```graphql mutation InternalCreateUser($user: InternalUserInput) { internal { createUser(user: $user) { success errors { message code ... on InvalidRecordError { model { apiIdentifier } validationErrors { message apiIdentifier } } } user } } gadgetMeta { hydrations(modelName: "user") } } ``` ```json { "user": {} } ``` #### Create records with a custom ID  By default, Forgelivo will automatically assign IDs to each record created. However, you can provide a custom ID for a record by passing the `id` field to `create` or `upsert` calls made with the internal API. ```typescript await api.internal.user.create({ id: "123", }); ``` ```graphql mutation InternalCreateUser($user: InternalUserInput) { internal { createUser(user: $user) { success errors { message code ... on InvalidRecordError { model { apiIdentifier } validationErrors { message apiIdentifier } } } user } } gadgetMeta { hydrations(modelName: "user") } } ``` ```json { "user": { "id": "123" } } ``` We do not recommend using custom IDs and when IDs are not globally unique, especially for multi-tenant applications. As an alternative, you can add a `Id` field to your model along with a [uniqueness validation](https://docs.forgelivo.com/guides/models/fields#uniqueness-validation), if valid, and use that field to store the custom ID. ### Create many records  Records can be bulk created through one call of the internal API through the `bulkCreate` method. Use the `api.internal.user.bulkCreate` JS client function or the `internal.bulkCreate` GraphQL field to create many records. ```typescript await api.internal.user.bulkCreate([{ id: 123 }, { id: 321 }]); ``` ```graphql mutation InternalBulkCreateUsers($users: [InternalUserInput]!) { internal { bulkCreateUsers(users: $users) { success errors { message code ... on InvalidRecordError { model { apiIdentifier } validationErrors { message apiIdentifier } } } users } } gadgetMeta { hydrations(modelName: "user") } } ``` ```json [{ "id": 123 }, { "id": 321 }] ``` ### Update a record  Records can be updated by ID using the internal API. ```typescript const userRecord = await api.internal.user.update(10, { // ... values }); // => a string console.log(userRecord.id); // => a string containing an ISO 8601 encoded Date console.log(userRecord.createdAt); // => now changed, a string containing an ISO 8601 encoded Date console.log(userRecord.updated); ``` ```graphql mutation InternalUpdateUser($id: GadgetID!, $user: InternalUserInput) { internal { updateUser(id: $id, user: $user) { success errors { message code ... on InvalidRecordError { model { apiIdentifier } validationErrors { message apiIdentifier } } } user } } gadgetMeta { hydrations(modelName: "user") } } ``` ```json { "id": 10, "user": {} } ``` ### Upsert a record  Records can be upserted by ID. If a record with the specified ID does not exist it will be created, if it does exist it will be updated. ```typescript await api.internal.user.upsert({ id: "123", // ... values }); ``` ```graphql mutation InternalUpsertUser($on: [String!], $user: InternalUserInput) { internal { upsertUser(on: $on, user: $user) { success errors { message code ... on InvalidRecordError { model { apiIdentifier } validationErrors { message apiIdentifier } } } user } } gadgetMeta { hydrations(modelName: "user") } } ``` ```json { "user": { "id": "123" } } ``` If your model has a field with a uniqueness validation you can also use that field (or fields) to upsert. For example if the `user` model has a field `uniqueField`: ```typescript await api.internal.user.upsert({ user: { uniqueField: "uniqueValue", // ... values }, on: ["uniqueField"], }); ``` ```graphql mutation InternalUpsertUser($on: [String!], $user: InternalUserInput) { internal { upsertUser(on: $on, user: $user) { success errors { message code ... on InvalidRecordError { model { apiIdentifier } validationErrors { message apiIdentifier } } } user } } gadgetMeta { hydrations(modelName: "user") } } ``` ```json { "user": { "uniqueField": "uniqueValue" }, "on": ["uniqueField"] } ``` ### Delete a record  Records can be deleted by ID using the internal API. ```typescript await api.internal.user.delete(10); ``` ```graphql mutation InternalDeleteUser($id: GadgetID!) { internal { deleteUser(id: $id) { success errors { message code ... on InvalidRecordError { model { apiIdentifier } validationErrors { message apiIdentifier } } } } } } ``` ```json { "id": 10 } ``` ### Delete many records  Records can be deleted by a set of filters using the internal API. Records are deleted performantly in batches under the hood, and like the rest of the internal API, no actions are run. Use the `api.internal.user.deleteMany` JS client function or the `internal.deleteManyUser` GraphQL field to delete records. ```typescript await api.internal.user.deleteMany(); ``` ```graphql mutation InternalDeleteManyUser { internal { deleteManyUser { success errors { message code ... on InvalidRecordError { model { apiIdentifier } validationErrors { message apiIdentifier } } } } } } ``` ```json {} ``` ```typescript await api.internal.user.deleteMany({ filter: { id: { greaterThan: 10 } } }); ``` ```graphql mutation InternalDeleteManyUser { internal { deleteManyUser { success errors { message code ... on InvalidRecordError { model { apiIdentifier } validationErrors { message apiIdentifier } } } } } } ``` ```json { "filter": { "id": { "greaterThan": 10 } } } ``` ```typescript await api.internal.user.deleteMany({ search: "some search" }); ``` ```graphql mutation InternalDeleteManyUser { internal { deleteManyUser { success errors { message code ... on InvalidRecordError { model { apiIdentifier } validationErrors { message apiIdentifier } } } } } } ``` ```json { "search": "some search" } ``` ### Atomically increment a record field  Records can be updated using the atomic increment operators for consistent, in-database updates to numeric fields. Atomic increments are very useful when trying to build counters that are potentially being written to by multiple processes at the same time. If you want to ensure that every addition to a counter is processed in serial, you can't use normal actions that process data in a read-modify-write cycle, as concurrent executions of the action might overwrite each other's newer data. Instead, passing an atomic operation to the internal API guarantees that these counter increments or decrements are each processed in serial within the database, ensuring data consistency. Atomic increments are triggered by passing an `_atomics: { someField: { increment: 10 } }` input object alongside any other fields you want to update in a single `create` or `update` mutation. ```typescript const record = await api.internal.exampleModel.update(10, { _atomics: { someNumericField: { increment: 5 }, }, // can also pass other fields for normal updates name: "new name", }); // is now 5 greater than it was before the call console.log(record.someNumericField); ``` ```graphql mutation IncrementExample($id: GadgetID!, $amount: Float!) { internal { updateExampleModel( id: $id exampleModel: { _atomics: { someNumericField: { increment: $amount } } } ) { success errors { message } exampleModel } } } ``` ```json { "id": 10, "amount": 42 } ``` Incrementing a field who's value is currently null will apply as if that field started at 0. ```typescript const record = await api.internal.exampleModel.create({ someNumericField: null, }); await api.internal.exampleModel.update(record.id, { _atomics: { someNumericField: { increment: 5 }, }, }); // is now set to 5 console.log(record.someNumericField); ``` ```graphql mutation IncrementExample($id: GadgetID!, $amount: Int!) { internal { updateExampleModel( id: $id exampleModel: { _atomics: { someNumericField: { increment: $amount } } } ) { success errors { message } exampleModel { id someNumericField } } } } ``` ```json { "id": 10, "amount": 5 } ``` ### Atomically decrement a record field  Similarly to the increment operator, records can be updated using the atomic decrement operators for consistent in-database updates to numeric fields. Atomic decrements are triggered by passing an `_atomics: { field: "someField", decrement: 10 }` input object alongside any other fields you want to update in a single `create` or `update` mutation. ```typescript const record = await api.internal.exampleModel.update(10, { _atomics: { someNumericField: { decrement: 5 }, }, // can also pass other fields for normal updates name: "new name", }); // is now 5 less than it was before the call console.log(record.someNumericField); ``` ```graphql mutation DecrementExample($id: GadgetID!, $amount: Int!) { internal { updateExampleModel( id: $id exampleModel: { _atomics: { someNumericField: { decrement: $amount } } } ) { success errors { message } exampleModel { id someNumericField } } } } ``` ```json { "id": 10, "amount": 42 } ``` Decrementing a field who's value is currently null will apply as if that field started at 0. ```typescript const record = await api.internal.exampleModel.create({ someNumericField: null, }); await api.internal.exampleModel.update(record.id, { _atomics: { someNumericField: { decrement: 5 }, }, }); // is now set to -5 console.log(record.someNumericField); ``` ```graphql mutation IncrementExample($id: GadgetID!, $amount: Int!) { internal { updateExampleModel( id: $id exampleModel: { _atomics: { someNumericField: { decrement: $amount } } } ) { success errors { message } exampleModel { id someNumericField } } } } ``` ```json { "id": 10, "amount": 5 } ``` ### Atomically change multiple fields  The internal API supports changing multiple fields simultaneously by passing multiple field names in the `_atomics` input object. Each field in the input can accept one command or a list of commands. ```typescript const record = await api.internal.exampleModel.update(10, { _atomics: { visitorCount: { increment: 1 }, availableTickets: [{ decrement: 1 }, { decrement: 0 }], }, // can also pass other fields for normal updates name: "new name", }); // is now 1 more than it was before the call console.log(record.visitorCount); // is now 1 less than it was before the call console.log(record.availableTickets); ``` ```graphql mutation MultipleAtomicsExample($id: GadgetID!) { internal { updateExampleModel( id: $id exampleModel: { _atomics: { visitorCount: { increment: 1 } availableTickets: [{ decrement: 1 }, { decrement: 0 }] } } ) { success errors { message } exampleModel { id visitorCount availableTickets } } } } ``` ```json { "id": 10 } ```