# Data sources

A custom block declares its **data sources** — semantic keys like `people` or `tasks` — in `custom_blocks.json`.

Data source mapping - resolving keys to ids - is stored on the block itself. This can be read/written programmatically using the `ncblock` CLI (try `npx ncblock --help`) or in the Notion UI.

At runtime, your code references the semantic key and the SDK handles the lookup for you. Use `useDataSource(key)` for the rows themselves (it also exposes the resolved schema), and `useManifest()` when you need the declared data-source keys and their declarations (e.g. picking a default key, or rendering a key switcher). For non-React renderers, `customBlock.getManifest()` exposes the same manifest, but row querying is currently only exposed through the React `useDataSource` hook.

## Pages within a data source

Each row returned by `useDataSource` is a `NotionDataSourcePage` — `{ id, propertiesById, propertiesByKey, update }`. Read property values through either of the two views:

- `propertiesByKey[key]` — keyed by the semantic property keys you declared in the manifest.
- `propertiesById[propertyId]` — keyed by the raw Notion property ID.

The four built-ins (`created_time`, `last_edited_time`, `created_by`, `last_edited_by`) are always present in `propertiesById` (and `collectionSchema.propertiesById`), never in the `*ByKey` views — they don't have semantic keys.

### Updating a row

Each page carries its own `update` helper:

```ts
await row.update({
  properties: {
    score: { type: "number", number: 8 }, // semantic key
  },
  icon: { type: "emoji", emoji: "✅" },
});
```

Because the helper is bound to its data source, you can write property values keyed by **either** semantic keys or raw IDs — the SDK resolves them before sending the request. The input and result shapes are `NotionDataSourcePageUpdateInput` and `NotionDataSourcePageUpdateResult`.

Use `row.update(...)` whenever you already have a row in hand. For pages you don't have a row for (e.g. you only have a `pageId`), drop down to the top-level [`pages` API](./pages.md) — it covers create / get / update / delete and accepts raw property IDs only.

## API

### `useDataSource(key, options?)`

```ts
function useDataSource(key: string, options?: { limit?: number }): UseDataSourceResult;

type UseDataSourceResult = {
  items: NotionDataSourcePage[];
  collectionSchema?: NotionCollectionSchema;
  propertySchemasById: { [propertyId: string]: NotionPropertySchema };
  propertySchemasByKey: { [key: string]: NotionPropertySchema | undefined };
  isLoading: boolean;
  hasMore: boolean;
  error?: CustomBlockQueryDataSourceErrorInfo;
};
```

Reads the data source mapped to `key`. `limit` defaults to 20 and is capped at 999. To show more rows, keep the desired limit in your own component state and pass the larger value back into `useDataSource(key, { limit })`. `propertySchemasByKey` is `undefined` for declared-but-unbound slots. When the host reports a query failure, `error` is `{ code, message, isRetryable }` with `code` narrowed to `CustomBlockQueryDataSourceErrorCode`.

### `useManifest()`

```ts
function useManifest(): CustomBlockManifest | null;
```

Returns the author-declared manifest loaded from `custom_blocks.json` — the semantic data-source keys plus their declared `name`, `description`, and property declarations. `null` when the block ships no manifest. This is the configuration the block _declared_, not host-resolved bindings; use `useDataSource(key)` for rows and resolved schema. Handy for enumerating declared keys:

```tsx
const manifest = useManifest();
const keys = Object.keys(manifest?.dataSources ?? {});
const activeKey = keys[0] ?? "default";
```

### `customBlock.getManifest()`

```ts
function customBlock.getManifest(): CustomBlockManifest | null;
```

Framework-neutral getter for the same manifest returned by `useManifest()`. The manifest is static for the lifetime of the sandbox, so there is nothing to subscribe to.

```ts
await initCustomBlock();

renderManifest(customBlock.getManifest());
```

`customBlock` does not yet expose a non-React equivalent of `useDataSource(key)`: querying rows, tracking `isLoading` / `hasMore`, and using row-level `update` helpers still require the React hook.

## Example: querying a data source

A typical data-driven view picks a key, calls `useDataSource`, schema-checks the rows, and surfaces a setup hint when the mapped collection is missing the expected fields. Trimmed from `templates/radar-chart`:

```tsx
import type { NotionDataSourcePage } from "ncblock";
import { useDataSource } from "ncblock/react";

const KEY = "people";

function isComplete(item: NotionDataSourcePage): boolean {
  return (
    typeof item.propertiesByKey.name === "string" &&
    typeof item.propertiesByKey.score === "number" &&
    Number.isFinite(item.propertiesByKey.score)
  );
}

export function ScoreList() {
  const [limit, setLimit] = useState(20);
  const { items, isLoading, hasMore, error } = useDataSource(KEY, { limit });

  if (error) return <div role="alert">Couldn't load: {error.message}</div>;
  if (isLoading && items.length === 0) return <div>Loading…</div>;

  const ready = items.filter(isComplete);
  if (ready.length === 0) {
    return (
      <div>
        Map a data source with key <code>{KEY}</code> exposing <code>name</code>{" "}
        (text) and <code>score</code> (number).
      </div>
    );
  }

  return (
    <div>
      <ul>
        {ready.map((item) => (
          <li key={item.id}>
            {String(item.propertiesByKey.name)} —{" "}
            {Number(item.propertiesByKey.score)}
          </li>
        ))}
      </ul>
      {hasMore ? (
        <button type="button" onClick={() => setLimit(limit + 20)} disabled={isLoading}>
          {isLoading ? "Loading…" : "Load more"}
        </button>
      ) : null}
    </div>
  );
}
```

## Types

### Rows & values

- `NotionDataSource` — a resolved data source: semantic key, `collectionSchema`, `propertyIdsByKey`, `propertySchemasById`.
- `NotionDataSourcePage` — a single row exposed to app code: `{ id, propertiesById, propertiesByKey, update }`.
- `NotionDataSourceValue` — the discriminated union of values that can appear inside `propertiesById[propertyId]`. Date values branch into the `NotionDateValue` union below.
- `NotionDataSourcePageUpdateInput` / `NotionDataSourcePageUpdateResult` — input and result for the per-page `update` helper.
- `UseDataSourceOptions` — options accepted by `useDataSource`, currently `{ limit?: number }`.

### Property schemas

- `NotionPropertySchema` — schema for a single property (type plus type-specific config like `select` options).
- `NotionPropertyType` — string-literal union of every supported property type.
- `NOTION_PROPERTY_TYPES` — runtime list of those literals (handy for switch coverage and validation).
- `NotionPropertyOption` — a single `select` / `multi_select` / `status` option (`{ id, name, color }`).
- `NotionPropertyColor` — the color literal used by options and groups.
- `NotionStatusGroup` — the `status` property's "To do / In progress / Done" grouping.
- `NotionDualProperty` — properties that have both a primary and a secondary axis (e.g. `unique_id` prefix + number).
- `NotionBuiltinPropertyId` — string-literal union of the four synthetic property IDs (`created_time`, `last_edited_time`, `created_by`, `last_edited_by`).
- `NOTION_BUILTIN_PROPERTY_IDS` — runtime list of the four built-in IDs.

### Collection schema & pointers

- `NotionCollectionSchema` — host-supplied schema for the bound collection, including raw property schemas.
- `NotionRecordPointer` — `{ id, table }`. Generic reference to any Notion record (page, block, collection row). Exported for convenience; `useDataSource` and the `pages` API don't take one as input.

### IDs

Branded string types — they're plain strings at runtime but TypeScript distinguishes them.

- `NotionDataSourceId`
- `NotionSpaceId`

### Date values

Returned wherever a date / date-range value appears (e.g. inside `propertiesByKey` for a `date` property). All-day values use `NotionDate*` shapes; values with a time component use `NotionDateTime*`.

- `NotionDateValue` — discriminated union covering every date-shaped value below.
- `NotionDate`
- `NotionDateRange`
- `NotionDateTime`
- `NotionDateTimeRange`
- `NotionDateReminder`
- `NotionDateTimeReminder`
- `NotionTimeReminder`
- `NotionNoReminder`
