LogoLogo
search
  • Overview
  • Installation
  • Getting Started
  • keyboard_arrow_down

    Concepts


    • Configuration
    • Resources
    • Versioning & Migrations
    • Environments
  • CLI Reference
  • Best Practices

  • function
    Serverless

    database
    Database & Storage
  • Overview
  • SQL Tables
  • File Storage
  • Task Queues
  • Key-Value Stores

  • lock
    Auth
  • Overview
  • Providers
  • User Management
  • Introduction to Groups

  • update
    Realtime

    receipt_long
    Logging

Resources

Resources are at the heart of $PLACEHOLDER_NAME$. To put it frankly, everything that isn't the platform itself is a resource; a user, your database, your auth provider, even your application itself.
To create a new resource, you must define one declaratively. Declarative programming is often contrasted with imperative programming; instead of specifying the steps how to create a certain resource, you specify the end result that you want, and $PLACEHOLDER_NAME$ will get you there. For example:
import PlaceholderName from "@n2d4/placeholder-name";

export const app = PlaceholderName.defineApplication({
id: "myApplication",
infrastructureProvider: "in-memory", // simply stores all data in memory. For production you should use a real provider
version: 1,
});
import PlaceholderName from "@n2d4/placeholder-name";

export const app = PlaceholderName.defineApplication({
id: "myApplication",
infrastructureProvider: "in-memory", // simply stores all data in memory. For production you should use a real provider
version: 1,
});
This declaration roughly translates into the following steps (simplified):
  • If an application with id myApplication already exists in the platform:
    1. Make sure the declaration in the code is the same or a higher version than the one in the backend.
    2. If the code version is higher, upgrade the backend to the code version. (See Versioning)
    3. Return the existing application.
  • If no such application exists yet:
    1. Create a new application with the given id and version.
    2. Return the new application.
In some cases, you may prefer creating, getting, and upgrading resources manually. For this, $PLACEHOLDER_NAME$ also provides the app.createTable, app.getTable, and app.updateTable functions.

File organizationlink

$PLACEHOLDER_NAME$ wants to make it as easy as possible to use resources anywhere. We strive to make the interface for defineTable as simple as the interface for ES6 new Map(), but as capable as any other database table.
So, just like with ES6 new Map(), you can either put your resource declarations into a separate file, or keep them where you need them. For example, if you have a file where you want to load & save data about flowers from your database, you could define the table inside that file:
// flowers.ts

const flowersTable = app.defineTable({
id: "flowers",
version: 1,
description: "A table of flowers",
columns: {
id: sql`uuid PRIMARY KEY`,
name: sql`varchar(255) NOT NULL`,
colorRgb: sql`varchar(6) NOT NULL`,
},
});

export async function loadFlowers(): Promise<Flower[]> {
const flowers = await flowersTable.list();
return flowers;
}

export async function addFlower(flower: Flower) {
await flowersTable.insert(flower);
}
// flowers.ts

const flowersTable = app.defineTable({
id: "flowers",
version: 1,
description: "A table of flowers",
columns: {
id: sql`uuid PRIMARY KEY`,
name: sql`varchar(255) NOT NULL`,
colorRgb: sql`varchar(6) NOT NULL`,
},
});

export async function loadFlowers(): Promise<Flower[]> {
const flowers = await flowersTable.list();
return flowers;
}

export async function addFlower(flower: Flower) {
await flowersTable.insert(flower);
}
Now the table definition is only accessible from the file, and part of the internal implementation details of flowers.ts. This guarantees that future modifications to the table scheme don't break any other code.
While you can define tables anywhere (inside a conditional, loop, function, or class constructor, for example), for code clarity reasons it is recommended to only define them at the top-level of each file. For more information on the trade-offs and how to use parametric resources, see the section below.
We encourage you to give this style of file organization a try, even if it is very different from what you've seen in other frameworks. Because all of $PLACEHOLDER_NAME$ was built around it, the issues that other frameworks have with file-local configuration don't exist here.

Deleting resourceslink

Deleting resources is special compared to creating and upgrading. To avoid accidental data loss, deletion is always an imperative two-step process:
  1. Update the codebase by either:
    • Removing the resource definition from the codebase (implicit deletion), or
    • Marking the resource for deletion (explicit deletion).
  2. Then, explicitly deleting the resource on the Dashboardopen_in_new or the CLI, eg. pn tables delete flowers (see why)

Implicit deletionlink

When you refer to a resource in your codebase, we call this a resource reference. Most typically, you refer to resources by the defineXYZ functions seen above.
As long as a running application references a resource, you will not be able to delete them. So, your first step is to remove all references from your codebase and redeploying your application. If you have a flowers.ts file like above, you can just delete the file (or make sure the defineTable call is not executed anymore).
Until you complete the deletion process on the Dashboard or the CLI (see above), you can always restore the table again by adding the resource reference back to your codebase.

Explicit deletionlink

You can explicitly mark a resource as deleted, like this:
const flowersTable = app.defineTable({
id: "flowers",
// as with any change, even when deleting a resource, you still need to bump the version
// see the docs on Versioning for more information
version: 2,
...versioned({
v1: {
// ...
},
v2: {
markForDeletion: true,
},
}),
markForDeletion: true,
});

console.log(flowersTable); // prints undefined
const flowersTable = app.defineTable({
id: "flowers",
// as with any change, even when deleting a resource, you still need to bump the version
// see the docs on Versioning for more information
version: 2,
...versioned({
v1: {
// ...
},
v2: {
markForDeletion: true,
},
}),
markForDeletion: true,
});

console.log(flowersTable); // prints undefined
It will now be inaccessible from your codebase. However, all data will remain in the database, and to actually delete that you have to delete it from the Dashboardopen_in_new or using the CLI:
$ pn tables delete flowers
$ pn tables delete flowers
Until you complete the deletion process on the Dashboard or the CLI (see above), you can always restore the table again by bumping the version number:
const flowersTable = app.defineTable({
id: "flowers",
version: 3,
...versioned({
v1: {
// ...
},
v2: {
markForDeletion: true,
},
v3: versioned.revertFrom("v1"),
}),
});
const flowersTable = app.defineTable({
id: "flowers",
version: 3,
...versioned({
v1: {
// ...
},
v2: {
markForDeletion: true,
},
v3: versioned.revertFrom("v1"),
}),
});

Parametric resourceslink

Sometimes you may want to create resources dynamically. A common example is organizations. We can create a parametric resource using defineParametricXYZ:
const organizationAdmins = app.defineParametricGroup({
// ...
});

const organizationMembers = app.defineParametricGroup({
id: "organizationMembers",
version: 1,
parameters: {
orgId: PNSchema.string(),
},
description: "A group that contains all members of the given organization",
}, ({ orgId }) => {
manager: organizationAdmins.where({ orgId }),

});

export function addUserToOrganization(user: User, orgId: string) {
return organizationMembers.where({ orgId }).addMember(user);
}
const organizationAdmins = app.defineParametricGroup({
// ...
});

const organizationMembers = app.defineParametricGroup({
id: "organizationMembers",
version: 1,
parameters: {
orgId: PNSchema.string(),
},
description: "A group that contains all members of the given organization",
}, ({ orgId }) => {
manager: organizationAdmins.where({ orgId }),

});

export function addUserToOrganization(user: User, orgId: string) {
return organizationMembers.where({ orgId }).addMember(user);
}
The id, version, and description properties are the same for all instances of the parametric resource, while anything else may vary.

Why parametric resources?link

...

Anatomy of a resourcelink

All resources have a few common properties:
  • type: The type of the resource, for example table or group.
  • parent: The parent resource. For example, a table's parent is the application it belongs to. Only the application resource has no parent.
  • id: A string identifier for the resource. It must be unique among all resources of the same type and parent, with the exception of parametric resources, where two resources with different parameters can have the same ID. It is recommended to use human-readable IDs, but some teams may choose to suffix them with a random string to avoid collisions.
  • uri: An automatically generated uniform resource identifieropen_in_new that points to the resource. It is globally unique, meaning that it is even unique across different projects. It is technically also a URLopen_in_new, but we refer to them as URIs to avoid confusion.
  • version: The version of the resource. See Versioning for more information.
  • description: A human-readable description of the resource. This is optional, but recommended.

LogoLogo