Resource

Every cloud resource is represented as a typed, schema-driven object.

Source: @notation/resource

Defining a resource

A resource is defined using the defineResource builder. The full chain looks like this:

import { defineResource } from "@notation/resource";
import { z } from "zod";

const LambdaFunction = defineResource<{
  Key: { FunctionName: string };
  CreateParams: { FunctionName: string; Runtime: string; Handler: string; Code: Buffer };
  UpdateParams: { FunctionName: string };
  ReadResult: { FunctionArn: string };
}>({ type: "aws/lambda/LambdaFunction" })
  .defineSchema({
    // params — input properties
    FunctionName: { propertyType: "param", valueType: z.string(), presence: "required", immutable: true },
    Runtime:      { propertyType: "param", valueType: z.string(), presence: "required" },
    Handler:      { propertyType: "param", valueType: z.string(), presence: "required", defaultValue: "index.handler" },
    MemorySize:   { propertyType: "param", valueType: z.number(), presence: "optional", defaultValue: 128 },

    // computed — populated by the cloud provider after creation
    FunctionArn:  { propertyType: "computed", valueType: z.string(), presence: "required", primaryKey: true },
  })
  .defineOperations({
    create: async (params) => { /* AWS SDK call, returns { FunctionArn } */ },
    read:   async (key) => { /* AWS SDK call */ },
    delete: async (key, state) => { /* AWS SDK call */ },
  });

Step by step

1. defineResource(schema)(meta)

schema declares the API shape that the rest of the chain must conform to:

FieldPurpose
KeyFields that identify the resource for read/update/delete
CreateParamsFields required to create the resource
UpdateParamsFields that can be patched after creation
ReadResultFields returned by the cloud provider on read

meta defines runtime values for state tracking, reconciliation, and display

FieldPurpose
typea platform/service/ResourceName string (e.g. aws/lambda/LambdaFunction).

2. resource({ type })type defines

3. defineSchema() — maps each field to a schema item. See Schema items below.

4. defineOperations() — CRUD handlers and error-handling config. See Operations below.

Schema items

Every field in the schema is a SchemaItem. Each item has a propertyType that determines its role:

// param — input, set when defining the resource
FunctionName: { propertyType: "param", valueType: z.string(), presence: "required", immutable: true }

// computed — output, set by the cloud provider after creation
FunctionArn: { propertyType: "computed", valueType: z.string(), presence: "required", primaryKey: true }

// derived — calculated from dependencies at deploy time via deriveParams()
IntegrationUri: { propertyType: "derived", valueType: z.string(), presence: "required" }

Common fields

All schema items carry these fields:

FieldTypeDescription
propertyType"param" | "computed" | "derived"Role of the field.
valueTypeZodTypeZod validator for type checking and serialisation.
presence"required" | "optional"Whether the field must be provided.
sensitivetrue?Redacted in logs.
hiddentrue?Excluded from CLI display and state output.
volatiletrue?Expected to change between reads; excluded from diff comparison.

Flags by property type

param

  • immutable — cannot change after creation (forces replacement).
  • defaultValue — fallback when no value is provided.
  • primaryKey / secondaryKey — together form the compound key used for read/update/delete.

computed

  • primaryKey — identifies the resource (e.g. FunctionArn returned by create).

Operations

defineOperations accepts CRUD handlers and error-handling configuration:

FieldRequiredSignature / Description
createyes(params: Params<S>) => Promise<ComputedPrimaryKey<S>> — create the resource, return its computed key.
readno(key: CompoundKey<S>) => Promise<Result<S>> — read current state.
updateno(key, patch, params, state) => Promise<void> — apply a partial update.
deleteyes(key, state) => Promise<void> — destroy the resource.
deriveParamsnoComputes intrinsic derived params from config (not dependency-aware).
retryReadOnConditionnoConditions on read output that trigger a retry (e.g. eventual consistency).
failOnErrornoError matchers that cause immediate failure with a reason.
notFoundOnErrornoError matchers that indicate the resource does not exist.
retryLaterOnErrornoError matchers that indicate a transient failure worth retrying.

Dependencies

A resource can depend on other resources. After defineOperations, chain requireDependencies and deriveParams:

const LambdaIntegration = defineResource<{ ... }>({ type: "aws/apiGateway/LambdaIntegration" })
  .defineSchema({ ... })
  .defineOperations({ ... })
  .requireDependencies<{ api: ApiResource; lambda: LambdaResource }>()
  .deriveParams(({ id, config, deps }) => ({
    IntegrationUri: deps.lambda.output.FunctionArn,
    ApiId: deps.api.output.ApiId,
  }));

requireDependencies declares typed dependency slots. deriveParams receives { id, config, deps } and returns properties computed from dependency outputs. The values are resolved at deploy time after dependencies have been provisioned.

Dependencies also determine deployment order: a resource is not created until all its dependencies exist.

Resource groups

A ResourceGroup bundles the low-level resources that make up a single logical construct. A Lambda group, for example, contains a Lambda function, an IAM role, a CloudWatch log group, and a zip package.

abstract class ResourceGroup {
  type: string;
  id: number;
  dependencies: Record<string, number>;
  config: Record<string, any>;
  resources: BaseResource[];

  add<T extends BaseResource>(resource: T): T;
  findResource<T>(ResourceClass: T): InstanceType<T> | undefined;
}

Construction accepts a ResourceGroupOptions:

  • collector — a ResourceCollector used during graph construction to allocate group IDs and register resources.
  • id — pre-assigned ID, used when no collector is provided.
  • dependencies — map of dependency names to group IDs.

When a collector is provided, the group registers itself and each resource added via add() is registered into the collector. This is how a construct like export const getTodos = lambda({ ... }) maps to the 4–6 actual AWS resources required to run it.

Resource groups are collected during graph construction and used by the reconciler to determine the full set of resources to deploy or destroy.