Simple Blog Example Internal 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.
What is the difference, and why two APIs?
The Public API and the Internal API for Simple Blog Example 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. 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 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 Simple Blog Example, 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.
No computed fields
The Internal API doesn't return values for computed fields to ensure performance. If you need the value of a computed field, you can use the Public API, or add a Global Action which retrieves it server side.
Support for atomic operations
The Public API for Simple Blog Example 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.
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 Simple Blog Example 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 Simple Blog Example 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 stringconsole.log(sessionRecord.createdAt); //=> a Date object
1query InternalFindSession($id: GadgetID!) {2 gadgetMeta {3 hydrations(modelName: "session")4 }56 internal {7 session(id: $id)8 }9}
{ "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 stringconsole.log(sessionRecords[0].createdAt); //=> a string containing an ISO 8601 encoded Date
1query InternalFindManySession(2 $sort: [SessionSort!]3 $filter: [SessionFilter!]4 $after: String5 $before: String6 $first: Int7 $last: Int8) {9 internal {10 listSession(11 sort: $sort12 filter: $filter13 after: $after14 before: $before15 first: $first16 last: $last17 ) {18 pageInfo {19 hasNextPage20 hasPreviousPage21 startCursor22 endCursor23 }24 edges {25 cursor26 node27 }28 }29 }30}
{ "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 stringconsole.log(sessionRecord.createdAt); //=> a Date object
1query InternalFindFirstSession(2 $sort: [SessionSort!]3 $filter: [SessionFilter!]4 $first: Int5) {6 internal {7 listSession(sort: $sort, filter: $filter, first: $first) {8 edges {9 node10 }11 }12 }13}
{ "first": 1, "sort": {}, "filter": {} }
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 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 stringconsole.log(sessionRecord.createdAt); //=> a string containing an ISO 8601 encoded Date
1fragment InternalErrorsDetails on ExecutionError {2 code3 message4 ... on InvalidRecordError {5 validationErrors {6 apiIdentifier7 message8 }9 model {10 apiIdentifier11 }12 record13 }14}1516mutation InternalCreateSession($record: InternalSessionInput) {17 gadgetMeta {18 hydrations(modelName: "session")19 }2021 internal {22 createSession(session: $record) {23 success24 errors {25 ...InternalErrorsDetails26 }27 session28 }29 }30}
{ "record": {} }
Update a record
Records can be updated by ID using the Internal API.
1const sessionRecord = await api.internal.session.update(10, {2 // ... values3});4console.log(sessionRecord.id); //=> a string5console.log(sessionRecord.createdAt); //=> a string containing an ISO 8601 encoded Date6console.log(sessionRecord.updated); //=> now changed, a string containing an ISO 8601 encoded Date
1fragment InternalErrorsDetails on ExecutionError {2 code3 message4 ... on InvalidRecordError {5 validationErrors {6 apiIdentifier7 message8 }9 model {10 apiIdentifier11 }12 record13 }14}1516mutation InternalUpdateSession($id: GadgetID!, $record: InternalSessionInput) {17 gadgetMeta {18 hydrations(modelName: "session")19 }2021 internal {22 updateSession(id: $id, session: $record) {23 success24 errors {25 ...InternalErrorsDetails26 }27 session28 }29 }30}
{ "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 code3 message4 ... on InvalidRecordError {5 validationErrors {6 apiIdentifier7 message8 }9 model {10 apiIdentifier11 }12 record13 }14}1516mutation InternalDeleteSession($id: GadgetID!) {17 gadgetMeta {18 hydrations(modelName: "session")19 }2021 internal {22 deleteSession(id: $id) {23 success24 errors {25 ...InternalErrorsDetails26 }27 }28 }29}
{ "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.
await api.internal.session.deleteMany();
1fragment InternalErrorsDetails on ExecutionError {2 code3 message4 ... on InvalidRecordError {5 validationErrors {6 apiIdentifier7 message8 }9 model {10 apiIdentifier11 }12 record13 }14}1516mutation InternalDeleteManySession($search: String, $filter: [SessionFilter!]) {17 gadgetMeta {18 hydrations(modelName: "session")19 }2021 internal {22 deleteManySession(search: $search, filter: $filter) {23 success24 errors {25 ...InternalErrorsDetails26 }27 }28 }29}
{}
await api.internal.session.deleteMany({ filter: [{ id: { greaterThan: 10 } }] });
1fragment InternalErrorsDetails on ExecutionError {2 code3 message4 ... on InvalidRecordError {5 validationErrors {6 apiIdentifier7 message8 }9 model {10 apiIdentifier11 }12 record13 }14}1516mutation InternalDeleteManySession($search: String, $filter: [SessionFilter!]) {17 gadgetMeta {18 hydrations(modelName: "session")19 }2021 internal {22 deleteManySession(search: $search, filter: $filter) {23 success24 errors {25 ...InternalErrorsDetails26 }27 }28 }29}
{ "filter": [{ "id": { "greaterThan": 10 } }] }
await api.internal.session.deleteMany({ search: "some search" });
1fragment InternalErrorsDetails on ExecutionError {2 code3 message4 ... on InvalidRecordError {5 validationErrors {6 apiIdentifier7 message8 }9 model {10 apiIdentifier11 }12 record13 }14}1516mutation InternalDeleteManySession($search: String, $filter: [SessionFilter!]) {17 gadgetMeta {18 hydrations(modelName: "session")19 }2021 internal {22 deleteManySession(search: $search, filter: $filter) {23 success24 errors {25 ...InternalErrorsDetails26 }27 }28 }29}
{ "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 updates6 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: $id5 exampleModel: { _atomics: { someNumericField: { increment: $amount } } }6 ) {7 success8 errors {9 message10 }11 exampleModel {12 id13 someNumericField14 }15 }16 }17}
{ "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: $id5 exampleModel: { _atomics: { someNumericField: { increment: $amount } } }6 ) {7 success8 errors {9 message10 }11 exampleModel {12 id13 someNumericField14 }15 }16 }17}
{ "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 updates6 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: $id5 exampleModel: { _atomics: { someNumericField: { decrement: $amount } } }6 ) {7 success8 errors {9 message10 }11 exampleModel {12 id13 someNumericField14 }15 }16 }17}
{ "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: $id5 exampleModel: { _atomics: { someNumericField: { decrement: $amount } } }6 ) {7 success8 errors {9 message10 }11 exampleModel {12 id13 someNumericField14 }15 }16 }17}
{ "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 updates7 name: "new name",8});9console.log(record.visitorCount); // is now 1 more than it was before the call10console.log(record.availableTickets); // is now 1 less than it was before the call
1mutation MultipleAtomicsExample($id: GadgetID!) {2 internal {3 updateExampleModel(4 id: $id5 exampleModel: {6 _atomics: {7 visitorCount: { increment: 1 }8 availableTickets: [{ decrement: 1 }, { decrement: 0 }]9 }10 }11 ) {12 success13 errors {14 message15 }16 exampleModel {17 id18 visitorCount19 availableTickets20 }21 }22 }23}
{ "id": 10 }