blog Internal API Endpoints

Gadget exposes a second, lower level set of GraphQL fields called the Internal API for blog. The Internal API lets trusted parties make changes that the Public API prohibits for maximum power and flexibility.

What is the difference, and why two APIs?

The Public API and the Internal API for blog exist for two different purposes. 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, preconditions, and state transitions. The Public API should be the default tool developers reach for.

That said, we don't live in a world where everything goes as planned. In a pinch, you may want to skip all the fancy bits to fix a bug, find a problem, or fish out some hidden data. The Internal API is for these developer-facing use cases that need to escape the happy path that the Public API implements. 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 actually make changes under the hood, and for developers, it is also the thing to use when you need to make changes under the hood.

It's generally advisable to avoid using the Internal API until you have no other choic because of it's low level nature. The Internal API is a sharp tool that can easily break data and violate important promises made by other parts of blog, so use it with care.

No actions

Actions are not available in the Internal API. If you would like to call an action, you must use the Public API. The Internal API only exposes low level create, read, update, and delete functionality.

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 blog 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 guaranteed to each apply 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.

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 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 blog JavaScript client supports making internal API calls as well which is documented below.

Authentication

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 blog 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 sessionRecord = await api.internal.session.findOne("some-id");
console.log(sessionRecord.id); //=> a string
console.log(sessionRecord.createdAt); //=> a Date object
1query InternalFindSession($id: GadgetID!) {
2 gadgetMeta {
3 hydrations(modelName: "session")
4 }
5
6 internal {
7 session(id: $id)
8 }
9}
Variables
json
{ "id": "some-id" }

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 sessionRecords = await api.internal.session.findMany({ sort: {}, filter: {} });
console.log(sessionRecords[0].id); //=> a string
console.log(sessionRecords[0].createdAt); //=> a string containing an ISO 8601 encoded Date
1query InternalFindManySession(
2 $sort: [SessionSort!]
3 $filter: [SessionFilter!]
4 $after: String
5 $before: String
6 $first: Int
7 $last: Int
8) {
9 internal {
10 listSession(
11 sort: $sort
12 filter: $filter
13 after: $after
14 before: $before
15 first: $first
16 last: $last
17 ) {
18 pageInfo {
19 hasNextPage
20 hasPreviousPage
21 startCursor
22 endCursor
23 }
24 edges {
25 cursor
26 node
27 }
28 }
29 }
30}
Variables
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.

const sessionRecord = await api.internal.session.findFirst({ sort: {}, filter: {} });
console.log(sessionRecord.id); //=> a string
console.log(sessionRecord.createdAt); //=> a Date object
1query InternalFindFirstSession(
2 $sort: [SessionSort!]
3 $filter: [SessionFilter!]
4 $first: Int
5) {
6 internal {
7 listSession(sort: $sort, filter: $filter, first: $first) {
8 edges {
9 node
10 }
11 }
12 }
13}
Variables
json
{ "first": 1, "sort": {}, "filter": {} }

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

const sessionRecord = await api.internal.session.create({
// ... values
});
console.log(sessionRecord.id); //=> a string
console.log(sessionRecord.createdAt); //=> a string containing an ISO 8601 encoded Date
1fragment InternalErrorsDetails on ExecutionError {
2 code
3 message
4 ... on InvalidRecordError {
5 validationErrors {
6 apiIdentifier
7 message
8 }
9 model {
10 apiIdentifier
11 }
12 record
13 }
14}
15
16mutation InternalCreateSession($record: InternalSessionInput) {
17 gadgetMeta {
18 hydrations(modelName: "session")
19 }
20
21 internal {
22 createSession(session: $record) {
23 success
24 errors {
25 ...InternalErrorsDetails
26 }
27 session
28 }
29 }
30}
Variables
json
{ "record": {} }

Update a record

Records can be updated by ID using the Internal API.

1const sessionRecord = await api.internal.session.update(10, {
2 // ... values
3});
4console.log(sessionRecord.id); //=> a string
5console.log(sessionRecord.createdAt); //=> a string containing an ISO 8601 encoded Date
6console.log(sessionRecord.updated); //=> now changed, a string containing an ISO 8601 encoded Date
1fragment InternalErrorsDetails on ExecutionError {
2 code
3 message
4 ... on InvalidRecordError {
5 validationErrors {
6 apiIdentifier
7 message
8 }
9 model {
10 apiIdentifier
11 }
12 record
13 }
14}
15
16mutation InternalUpdateSession($id: GadgetID!, $record: InternalSessionInput) {
17 gadgetMeta {
18 hydrations(modelName: "session")
19 }
20
21 internal {
22 updateSession(id: $id, session: $record) {
23 success
24 errors {
25 ...InternalErrorsDetails
26 }
27 session
28 }
29 }
30}
Variables
json
{ "id": 10, "record": {} }

Delete a record

Records can be deleted by ID using the Internal API.

await api.internal.session.delete(10);
1fragment InternalErrorsDetails on ExecutionError {
2 code
3 message
4 ... on InvalidRecordError {
5 validationErrors {
6 apiIdentifier
7 message
8 }
9 model {
10 apiIdentifier
11 }
12 record
13 }
14}
15
16mutation InternalDeleteSession($id: GadgetID!) {
17 gadgetMeta {
18 hydrations(modelName: "session")
19 }
20
21 internal {
22 deleteSession(id: $id) {
23 success
24 errors {
25 ...InternalErrorsDetails
26 }
27 }
28 }
29}
Variables
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.session.deleteMany JS client function or the internal.deleteManySession GraphQL field to delete records.

Delete all Session records
await api.internal.session.deleteMany();
1fragment InternalErrorsDetails on ExecutionError {
2 code
3 message
4 ... on InvalidRecordError {
5 validationErrors {
6 apiIdentifier
7 message
8 }
9 model {
10 apiIdentifier
11 }
12 record
13 }
14}
15
16mutation InternalDeleteManySession($search: String, $filter: [SessionFilter!]) {
17 gadgetMeta {
18 hydrations(modelName: "session")
19 }
20
21 internal {
22 deleteManySession(search: $search, filter: $filter) {
23 success
24 errors {
25 ...InternalErrorsDetails
26 }
27 }
28 }
29}
Variables
json
{}
Delete all records with ids greater than 10
await api.internal.session.deleteMany({ filter: [{ id: { greaterThan: 10 } }] });
1fragment InternalErrorsDetails on ExecutionError {
2 code
3 message
4 ... on InvalidRecordError {
5 validationErrors {
6 apiIdentifier
7 message
8 }
9 model {
10 apiIdentifier
11 }
12 record
13 }
14}
15
16mutation InternalDeleteManySession($search: String, $filter: [SessionFilter!]) {
17 gadgetMeta {
18 hydrations(modelName: "session")
19 }
20
21 internal {
22 deleteManySession(search: $search, filter: $filter) {
23 success
24 errors {
25 ...InternalErrorsDetails
26 }
27 }
28 }
29}
Variables
json
{ "filter": [{ "id": { "greaterThan": 10 } }] }
Delete all records matching a string search
await api.internal.session.deleteMany({ search: "some search" });
1fragment InternalErrorsDetails on ExecutionError {
2 code
3 message
4 ... on InvalidRecordError {
5 validationErrors {
6 apiIdentifier
7 message
8 }
9 model {
10 apiIdentifier
11 }
12 record
13 }
14}
15
16mutation InternalDeleteManySession($search: String, $filter: [SessionFilter!]) {
17 gadgetMeta {
18 hydrations(modelName: "session")
19 }
20
21 internal {
22 deleteManySession(search: $search, filter: $filter) {
23 success
24 errors {
25 ...InternalErrorsDetails
26 }
27 }
28 }
29}
Variables
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.

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});
8console.log(record.someNumericField); // is now 5 greater than it was before the call
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": 42 }

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});
9console.log(record.someNumericField); // is now set to 5
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 }

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});
8console.log(record.someNumericField); // is now 5 less than it was before the call
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 }

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});
9console.log(record.someNumericField); // is now set to -5
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 }

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});
9console.log(record.visitorCount); // is now 1 more than it was before the call
10console.log(record.availableTickets); // is now 1 less than it was before the call
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 }