example-app Internal vs public API endpoints 

Gadget 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.

When to use the public API 

The public API facilitates normal, expected requests, and should be used for all of the experiences powered by Gadget. 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 Gadget 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. The internal API can be used by running api.internal.someModel.<action name>. 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.

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, 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 

Unlike the public API, access to the internal API is granted as one top-level permission in the Gadget editor. The Internal API Access permission must be granted to a role the actor making the API call has for the call to succeed.

By default, only the Sysadmin role has permission to execute internal API requests. To grant an API key permission to make internal API requests, you could add the Sysadmin role to that API key, or create a new role with the Internal API Access permission granted, and add that role to a trusted API key.

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 section.

Once installed, internal API requests are made using the client.internal.<model name>.<action name> 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 Gadget 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.<modelName> 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.

const userRecord = await api.internal.user.findOne("123");
// => a string
console.log(userRecord.id);
// => a Date object
console.log(userRecord.createdAt);
1query InternalFindUser($id: GadgetID!, $select: [String!]) {
2 internal {
3 user(id: $id, select: $select)
4 }
5 gadgetMeta {
6 hydrations(modelName: "user")
7 }
8}
Variables
json
{ "id": "123" }
const userRecord = await api.internal.user.findOne("123");
// => a string
console.log(userRecord.id);
// => a Date object
console.log(userRecord.createdAt);

Read many raw records 

Lists of records can be fetched via the internal API using the findMany JS function or the internal.list<modelName> 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.

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);
1query InternalFindManyUser($sort: [UserSort!], $filter: [UserFilter!]) {
2 internal {
3 listUser(sort: $sort, filter: $filter) {
4 pageInfo {
5 hasNextPage
6 hasPreviousPage
7 startCursor
8 endCursor
9 }
10 edges {
11 cursor
12 node
13 }
14 }
15 }
16 gadgetMeta {
17 hydrations(modelName: "user")
18 }
19}
Variables
json
{ "sort": {}, "filter": {} }
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);

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.

const userRecord = await api.internal.user.findFirst({ sort: {}, filter: {} });
// => a string
console.log(userRecord.id);
// => a Date object
console.log(userRecord.createdAt);
1query InternalFindFirstUser(
2 $sort: [UserSort!]
3 $filter: [UserFilter!]
4 $first: Int
5) {
6 internal {
7 listUser(sort: $sort, filter: $filter, first: $first) {
8 edges {
9 node
10 }
11 }
12 }
13 gadgetMeta {
14 hydrations(modelName: "user")
15 }
16}
Variables
json
{ "first": 1, "sort": {}, "filter": {} }
const userRecord = await api.internal.user.findFirst({ sort: {}, filter: {} });
// => a string
console.log(userRecord.id);
// => a Date object
console.log(userRecord.createdAt);

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:

// only return the id and createdAt field on the post record
const post = await api.internal.user.findFirst({
select: { id: true, createdAt: true },
});
query {
internal {
user(select: { id: true, createdAt: true })
}
}
// only return the id and createdAt field on the post record
const post = await api.internal.user.findFirst({
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 creates, updates, and deletes 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.<modelName>.<verb> 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.

1const userRecord = await api.internal.user.create({
2 // ... values
3});
4// => a string
5console.log(userRecord.id);
6// => a string containing an ISO 8601 encoded Date
7console.log(userRecord.createdAt);
1mutation InternalCreateUser($user: InternalUserInput) {
2 internal {
3 createUser(user: $user) {
4 success
5 errors {
6 message
7 code
8 ... on InvalidRecordError {
9 model {
10 apiIdentifier
11 }
12 validationErrors {
13 message
14 apiIdentifier
15 }
16 }
17 }
18 user
19 }
20 }
21 gadgetMeta {
22 hydrations(modelName: "user")
23 }
24}
Variables
json
{ "user": {} }
1const userRecord = await api.internal.user.create({
2 // ... values
3});
4// => a string
5console.log(userRecord.id);
6// => a string containing an ISO 8601 encoded Date
7console.log(userRecord.createdAt);

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.

await api.internal.user.bulkCreate([{ id: 123 }, { id: 321 }]);
1mutation InternalBulkCreateUsers($users: [InternalUserInput]!) {
2 internal {
3 bulkCreateUsers(users: $users) {
4 success
5 errors {
6 message
7 code
8 ... on InvalidRecordError {
9 model {
10 apiIdentifier
11 }
12 validationErrors {
13 message
14 apiIdentifier
15 }
16 }
17 }
18 users
19 }
20 }
21 gadgetMeta {
22 hydrations(modelName: "user")
23 }
24}
Variables
json
[{ "id": 123 }, { "id": 321 }]
await api.internal.user.bulkCreate([{ id: 123 }, { id: 321 }]);

Update a record 

Records can be updated by ID using the internal API.

1const userRecord = await api.internal.user.update(10, {
2 // ... values
3});
4// => a string
5console.log(userRecord.id);
6// => a string containing an ISO 8601 encoded Date
7console.log(userRecord.createdAt);
8// => now changed, a string containing an ISO 8601 encoded Date
9console.log(userRecord.updated);
1mutation InternalUpdateUser($id: GadgetID!, $user: InternalUserInput) {
2 internal {
3 updateUser(id: $id, user: $user) {
4 success
5 errors {
6 message
7 code
8 ... on InvalidRecordError {
9 model {
10 apiIdentifier
11 }
12 validationErrors {
13 message
14 apiIdentifier
15 }
16 }
17 }
18 user
19 }
20 }
21 gadgetMeta {
22 hydrations(modelName: "user")
23 }
24}
Variables
json
{ "id": 10, "user": {} }
1const userRecord = await api.internal.user.update(10, {
2 // ... values
3});
4// => a string
5console.log(userRecord.id);
6// => a string containing an ISO 8601 encoded Date
7console.log(userRecord.createdAt);
8// => now changed, a string containing an ISO 8601 encoded Date
9console.log(userRecord.updated);

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.

await api.internal.user.upsert({
id: "123",
// ... values
});
1mutation InternalUpsertUser($on: [String!], $user: InternalUserInput) {
2 internal {
3 upsertUser(on: $on, user: $user) {
4 success
5 errors {
6 message
7 code
8 ... on InvalidRecordError {
9 model {
10 apiIdentifier
11 }
12 validationErrors {
13 message
14 apiIdentifier
15 }
16 }
17 }
18 user
19 }
20 }
21 gadgetMeta {
22 hydrations(modelName: "user")
23 }
24}
Variables
json
{ "user": { "id": "123" } }
await api.internal.user.upsert({
id: "123",
// ... values
});

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:

1await api.internal.user.upsert({
2 user: {
3 uniqueField: "uniqueValue",
4 // ... values
5 },
6 on: ["uniqueField"],
7});
1mutation InternalUpsertUser($on: [String!], $user: InternalUserInput) {
2 internal {
3 upsertUser(on: $on, user: $user) {
4 success
5 errors {
6 message
7 code
8 ... on InvalidRecordError {
9 model {
10 apiIdentifier
11 }
12 validationErrors {
13 message
14 apiIdentifier
15 }
16 }
17 }
18 user
19 }
20 }
21 gadgetMeta {
22 hydrations(modelName: "user")
23 }
24}
Variables
json
{ "user": { "uniqueField": "uniqueValue" }, "on": ["uniqueField"] }
1await api.internal.user.upsert({
2 user: {
3 uniqueField: "uniqueValue",
4 // ... values
5 },
6 on: ["uniqueField"],
7});

Delete a record 

Records can be deleted by ID using the internal API.

await api.internal.user.delete(10);
1mutation InternalDeleteUser($id: GadgetID!) {
2 internal {
3 deleteUser(id: $id) {
4 success
5 errors {
6 message
7 code
8 ... on InvalidRecordError {
9 model {
10 apiIdentifier
11 }
12 validationErrors {
13 message
14 apiIdentifier
15 }
16 }
17 }
18 }
19 }
20}
Variables
json
{ "id": 10 }
await api.internal.user.delete(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.

Delete all user records
await api.internal.user.deleteMany();
1mutation InternalDeleteManyUser {
2 internal {
3 deleteManyUser {
4 success
5 errors {
6 message
7 code
8 ... on InvalidRecordError {
9 model {
10 apiIdentifier
11 }
12 validationErrors {
13 message
14 apiIdentifier
15 }
16 }
17 }
18 }
19 }
20}
Variables
json
{}
await api.internal.user.deleteMany();
Delete all records with ids greater than 10
await api.internal.user.deleteMany({ filter: { id: { greaterThan: 10 } } });
1mutation InternalDeleteManyUser {
2 internal {
3 deleteManyUser {
4 success
5 errors {
6 message
7 code
8 ... on InvalidRecordError {
9 model {
10 apiIdentifier
11 }
12 validationErrors {
13 message
14 apiIdentifier
15 }
16 }
17 }
18 }
19 }
20}
Variables
json
{ "filter": { "id": { "greaterThan": 10 } } }
await api.internal.user.deleteMany({ filter: { id: { greaterThan: 10 } } });
Delete all records matching a string search
await api.internal.user.deleteMany({ search: "some search" });
1mutation InternalDeleteManyUser {
2 internal {
3 deleteManyUser {
4 success
5 errors {
6 message
7 code
8 ... on InvalidRecordError {
9 model {
10 apiIdentifier
11 }
12 validationErrors {
13 message
14 apiIdentifier
15 }
16 }
17 }
18 }
19 }
20}
Variables
json
{ "search": "some search" }
await api.internal.user.deleteMany({ 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.

1const record = await api.internal.exampleModel.update(10, {
2 _atomics: {
3 someNumericField: { increment: 5 },
4 },
5 // can also pass other fields for normal updates
6 name: "new name",
7});
8// is now 5 greater than it was before the call
9console.log(record.someNumericField);
1mutation IncrementExample($id: GadgetID!, $amount: Float!) {
2 internal {
3 updateExampleModel(
4 id: $id
5 exampleModel: { _atomics: { someNumericField: { increment: $amount } } }
6 ) {
7 success
8 errors {
9 message
10 }
11 exampleModel
12 }
13 }
14}
Variables
json
{ "id": 10, "amount": 42 }
1const record = await api.internal.exampleModel.update(10, {
2 _atomics: {
3 someNumericField: { increment: 5 },
4 },
5 // can also pass other fields for normal updates
6 name: "new name",
7});
8// is now 5 greater than it was before the call
9console.log(record.someNumericField);

Incrementing a field who's value is currently null will apply as if that field started at 0.

1const record = await api.internal.exampleModel.create({
2 someNumericField: null,
3});
4await api.internal.exampleModel.update(record.id, {
5 _atomics: {
6 someNumericField: { increment: 5 },
7 },
8});
9// is now set to 5
10console.log(record.someNumericField);
1mutation IncrementExample($id: GadgetID!, $amount: Int!) {
2 internal {
3 updateExampleModel(
4 id: $id
5 exampleModel: { _atomics: { someNumericField: { increment: $amount } } }
6 ) {
7 success
8 errors {
9 message
10 }
11 exampleModel {
12 id
13 someNumericField
14 }
15 }
16 }
17}
Variables
json
{ "id": 10, "amount": 5 }
1const record = await api.internal.exampleModel.create({
2 someNumericField: null,
3});
4await api.internal.exampleModel.update(record.id, {
5 _atomics: {
6 someNumericField: { increment: 5 },
7 },
8});
9// is now set to 5
10console.log(record.someNumericField);

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.

1const record = await api.internal.exampleModel.update(10, {
2 _atomics: {
3 someNumericField: { decrement: 5 },
4 },
5 // can also pass other fields for normal updates
6 name: "new name",
7});
8// is now 5 less than it was before the call
9console.log(record.someNumericField);
1mutation DecrementExample($id: GadgetID!, $amount: Int!) {
2 internal {
3 updateExampleModel(
4 id: $id
5 exampleModel: { _atomics: { someNumericField: { decrement: $amount } } }
6 ) {
7 success
8 errors {
9 message
10 }
11 exampleModel {
12 id
13 someNumericField
14 }
15 }
16 }
17}
Variables
json
{ "id": 10, "amount": 42 }
1const record = await api.internal.exampleModel.update(10, {
2 _atomics: {
3 someNumericField: { decrement: 5 },
4 },
5 // can also pass other fields for normal updates
6 name: "new name",
7});
8// is now 5 less than it was before the call
9console.log(record.someNumericField);

Decrementing a field who's value is currently null will apply as if that field started at 0.

1const record = await api.internal.exampleModel.create({
2 someNumericField: null,
3});
4await api.internal.exampleModel.update(record.id, {
5 _atomics: {
6 someNumericField: { decrement: 5 },
7 },
8});
9// is now set to -5
10console.log(record.someNumericField);
1mutation IncrementExample($id: GadgetID!, $amount: Int!) {
2 internal {
3 updateExampleModel(
4 id: $id
5 exampleModel: { _atomics: { someNumericField: { decrement: $amount } } }
6 ) {
7 success
8 errors {
9 message
10 }
11 exampleModel {
12 id
13 someNumericField
14 }
15 }
16 }
17}
Variables
json
{ "id": 10, "amount": 5 }
1const record = await api.internal.exampleModel.create({
2 someNumericField: null,
3});
4await api.internal.exampleModel.update(record.id, {
5 _atomics: {
6 someNumericField: { decrement: 5 },
7 },
8});
9// is now set to -5
10console.log(record.someNumericField);

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.

1const record = await api.internal.exampleModel.update(10, {
2 _atomics: {
3 visitorCount: { increment: 1 },
4 availableTickets: [{ decrement: 1 }, { decrement: 0 }],
5 },
6 // can also pass other fields for normal updates
7 name: "new name",
8});
9// is now 1 more than it was before the call
10console.log(record.visitorCount);
11// is now 1 less than it was before the call
12console.log(record.availableTickets);
1mutation MultipleAtomicsExample($id: GadgetID!) {
2 internal {
3 updateExampleModel(
4 id: $id
5 exampleModel: {
6 _atomics: {
7 visitorCount: { increment: 1 }
8 availableTickets: [{ decrement: 1 }, { decrement: 0 }]
9 }
10 }
11 ) {
12 success
13 errors {
14 message
15 }
16 exampleModel {
17 id
18 visitorCount
19 availableTickets
20 }
21 }
22 }
23}
Variables
json
{ "id": 10 }
1const record = await api.internal.exampleModel.update(10, {
2 _atomics: {
3 visitorCount: { increment: 1 },
4 availableTickets: [{ decrement: 1 }, { decrement: 0 }],
5 },
6 // can also pass other fields for normal updates
7 name: "new name",
8});
9// is now 1 more than it was before the call
10console.log(record.visitorCount);
11// is now 1 less than it was before the call
12console.log(record.availableTickets);

Was this page helpful?