Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,69 @@ Every metadata directory is pre-wired into `objectstack.config.ts`, empty ones
included: add your entry to the named array in your own `src/<type>/index.ts` and
leave the config alone. It is the one file parallel branches collide on.

## Import your existing list

Every customer already has the list — a spreadsheet per role, usually. Getting
it in is the platform's **Import** button on each object list, not anything
Duly wrote: upload, confirm the mapping, import. The CSVs in
[`samples/`](samples) are shaped to go straight through it, and they describe
the same fictional manufacturer as `pnpm demo`.

| Sample | Import it on | Rows |
|:---|:---|:---|
| [`samples/business-units.csv`](samples/business-units.csv) | Setup → People & Organization → Business Units | 6 |
| [`samples/people.csv`](samples/people.csv) | Setup → People & Organization → Users | 12 |
| [`samples/catalog-items.csv`](samples/catalog-items.csv) | Duly → Setup → Role catalog | 21 |
| [`samples/duties.csv`](samples/duties.csv) | Duly → Setup → All duties | 19 |

### Three steps

1. **Put your people and units in first.** A duty's owner and business unit are
looked up **by name**, so the rows have to exist before the duty file can
land. In a real deployment they arrive from your directory; on a fresh
`pnpm dev` database, import `business-units.csv` and then `people.csv`
through the same Import button.
2. **Import the role catalog** — `catalog-items.csv` on Role catalog. This is
the list itself: what each position owes, how often, with how much grace.
No lookups, so it goes into an empty app as-is.
3. **Import the duties** — `duties.csv` on All duties. This is the catalog
instantiated onto named people, and it is where the natural keys resolve.

Each step is the same three screens — Upload → Mapping → Preview → import —
and the count of created rows is reported at the end, with any refused row
named and downloadable.

### What the columns have to say

- **Headers are field API names** (`position_code`, `due_offset_days`). The
wizard auto-matches every one of them at high confidence. The **Download
template** link on the Upload step gives you the same columns as labels
instead; both are accepted.
- **Lookups are written as names, not ids.** `owner` takes a person's name
(`Priya Raman`) or their email; `business_unit` takes the unit's **name**
(`Northgate Quality` — its code will not resolve); `catalog_item` takes the
catalog item's name. This is the same natural-key rule the seed loader uses.
- **A name that matches nothing skips that row and says so**, with a
*Download failed rows* file to fix and re-import. Nothing is linked to a
best guess.
- **Blank means "leave unset"**, so the object's defaults apply. That is what
lets one file carry all three duty forms: a `standing` row leaves the five
cadence columns empty and lands with them all null, which is exactly what
`standing_no_frequency` requires.
- **Read-only columns are never written.** They are visible in the mapping
step as `— Skip —` or `(match only)`, so a column that will not land says so
before you import.
- **Re-importing needs the match option.** *When a row matches an existing
record* defaults to *Always create new*; running the same file twice
otherwise gives you two copies.

`test/import-samples.test.ts` holds every sample header to the object's own
schema, so renaming a field fails the build instead of quietly importing a
blank column.

**[The full walk, screen by screen, with what each step was measured to
do →](docs/import/walkthrough.md)**

## Verify before you ship

```bash
Expand Down Expand Up @@ -101,14 +164,17 @@ src/security/ positions, permission sets, sharing rules
src/mappings/ src/data/ catalog import and seed fixtures
src/translations/ en (source) · zh-CN
scripts/ pnpm demo — prepare the database, then start with the example loaded
samples/ CSVs for the platform's standard Import (see above)
docs/product/ positioning, data model, design principles
docs/import/ the recorded import walk, screen by screen
```

## Documentation

- [Positioning](docs/product/positioning.md) — who this is for and what it is not
- [Data model](docs/product/data-model.md) — the five objects and why each exists
- [Design principles](docs/product/design-principles.md) — the constraints above, argued
- [Importing an existing list](docs/import/walkthrough.md) — the standard Import, walked and measured
- [Roadmap](docs/roadmap.md) — milestones M0–M4

## License
Expand Down
Binary file added docs/import/01-upload.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/import/02-mapping.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/import/03-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/import/04-result-catalog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/import/05-mapping-duties.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/import/06-result-duties.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/import/07-duties-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/import/08-unresolved-lookup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/import/09-result-business-units.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/import/10-result-people.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
213 changes: 213 additions & 0 deletions docs/import/walkthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# Importing an existing duty list — the recorded walk

What a pre-sales demo of "import the list you already have" actually looks
like, screen by screen, and what the platform's Import was **measured** to do
with each column.

Duly writes no import code. The **Import** button on every object list is the
platform's (`@objectstack/connector-rest` 17.2.0); everything below is that
button, driven in a browser against a clean database.

- **Recorded on:** `pnpm dev` (not `pnpm demo` — see [Why a clean
database](#why-a-clean-database)), fresh `.objectstack/data`, signed in as
the seeded `admin@objectos.ai`.
- **Files:** the four CSVs in [`samples/`](../../samples).
- **Result:** 6 + 12 + 21 + 19 rows, `0 skipped`.

---

## The wizard

Three screens, identical on every object.

### 1. Upload

`Drag & drop a CSV or Excel file here` — plus a **Download template** link that
emits a CSV with the object's columns, required ones marked `*`.

![Import step 1 — upload](01-upload.png)

The template writes **labels** as headers (`Duty *`, `Offset (days, 0 = anchor
day)`). The samples in this repo write **API names** (`name`,
`due_offset_days`) instead — both auto-match, and API names are stable across
locales, which is why the samples use them.

### 2. Mapping

![Import step 2 — mapping](02-mapping.png)

`Auto-matched 11 column(s)` for `catalog-items.csv` and 14 for `duties.csv` —
every column, all at *High confidence*, nothing to adjust by hand.

Two statuses are worth knowing, because both mean "this column will not be
written" and neither is an error:

| Shown as | Example | Meaning |
|:---|:---|:---|
| `— Skip —` / `Skipped` | `sys_user.manager_id` | The column cannot be mapped at all. The row still imports, without it. |
| `… (match only)` | `duly_duty.last_dispatched_period` | Mappable as a *match key* for update/upsert, never written. Measured: the import reported `1 created` and the column read back `null`. |

Both are read-only fields. **A skipped column is the quiet failure mode this
whole page exists to make loud** — the import succeeds, the records land, and
one column is simply blank. `test/import-samples.test.ts` fails the build if a
sample header ever stops naming a writable field.

### 3. Preview

![Import step 3 — preview](03-preview.png)

The parsed rows, plus the import options. The one that matters for a repeat
run:

**When a row matches an existing record** — `Always create new` (the default),
`Update existing (skip if no match)`, `Update if matched, else create`. On the
default, running the same file twice gives you two copies. Re-importing over an
earlier load means picking one of the other two and matching on `name`.

The button is labelled with the count: `Import 21 Rows`.

### Result

![Role catalog imported — 21 created](04-result-catalog.png)

---

## The four files, in order

Order is not a style preference. Each file's lookups resolve against rows the
previous file created.

### 1 · Business units — `samples/business-units.csv` → `sys_business_unit`

Setup → People & Organization → **Business Units** → Import.

![6 business units created](09-result-business-units.png)

`parent_business_unit_id` is written as the **parent's name**, and resolves
*within the same file* — `Northgate Plant` finds `Ardenline Group` from a row
above it. (The importer flushes pending creates and retries a miss on the same
object, so a child after its parent is safe.)

### 2 · People — `samples/people.csv` → `sys_user`

Setup → People & Organization → **Users** → Import.

![12 users created](10-result-people.png)

Name and email only. These are **directory rows, not logins** — nobody can sign
in as them. Real people get invited through Setup → Users → Invite User.

`manager_id` is deliberately **not** a column: it is read-only and the mapping
step drops it to `— Skip —`. Set the reporting chain in Setup, not in the CSV.

### 3 · Role catalog — `samples/catalog-items.csv` → `duly_catalog_item`

Duly → Setup → **Role catalog** → Import. 21 rows, no lookups at all — this
file imports into an otherwise empty database.

### 4 · Duties — `samples/duties.csv` → `duly_duty`

Duly → Setup → **All duties** → Import. Three lookups per row, all by natural
key.

![Import Duty — 19 created](06-result-duties.png)

![All duties — 19 records with owners resolved](07-duties-list.png)

Every `owner` resolved to a person, every `business_unit` to a unit, every
`catalog_item` to a template — read back through the API to confirm it is the
right row and not merely *a* row:

```
Lifting equipment check — Line C owner=Yuki Tanabe bu=Northgate Operations catalog_item=Lifting equipment check
Keep the permit register current — Ardenline owner=Nadia Ilves bu=Ardenline Group catalog_item=Keep the permit register current
Monthly site performance note owner=Tomas Bergh bu=Northgate Plant catalog_item=null
```

---

## How lookups resolve — measured, not assumed

The importer tries, in order: an exact `id`, the target object's display field,
then `name`, `title`, `label`, `full_name`, `email`, `username`. The first
field to match wins; **more than one match stops the row rather than linking
the first**.

| Column | Target | Write it as | Measured |
|:---|:---|:---|:---|
| `duly_duty.owner` | `sys_user` | the person's **name** (`Priya Raman`) | resolves |
| `duly_duty.owner` | `sys_user` | their **email** (`priya.raman@ardenline.example`) | resolves |
| `duly_duty.business_unit` | `sys_business_unit` | the unit's **name** (`Northgate Quality`) | resolves |
| `duly_duty.business_unit` | `sys_business_unit` | the unit's **code** (`NGP-QA`) | **does not resolve** |
| `duly_duty.catalog_item` | `duly_catalog_item` | the item's **name** | resolves |
| `sys_business_unit.parent_business_unit_id` | `sys_business_unit` | the parent's **name** | resolves, same file |

**This is the same rule the seed loader uses.** `src/data/org.seed.ts` resolves
`duly_task.owner` and `duly_duty.owner` as natural keys against `sys_user.name`;
the Import UI was the open question, and it matches. One format for both paths.

### When it does not resolve

The row is skipped and named. Nothing is guessed, nothing is left dangling.

![1 created, 2 skipped, with the reason per row](08-unresolved-lookup.png)

```
Row 2: Owner: No matching record for "Nobody Here"
Row 3: Business unit: No matching record for "NGP-QA"
```

**Download failed rows** hands back just those rows, so a partial load is
finished by fixing that file and importing it again.

This is also exactly what happens if you skip steps 1 and 2. Measured, on a
genuinely clean database with one user (`Dev Admin`) and no business units:
`duties.csv` imports **0 created, 19 skipped**, one `Owner: No matching record`
per row. Loud, per-row, recoverable — but the people have to be there first.

---

## Blank cells and the conditional defaults

A blank cell means **"leave this field unset"**, not "write null". The object's
`defaultValue` then decides, which is what makes one flat CSV able to carry
three duty forms without tripping the cadence rules in
`src/objects/catalog-item.object.ts`:

| Row form | Blank cadence cells | Landed as |
|:---|:---|:---|
| `standing` | frequency, anchor, offset, lead, grace | all five `null` — `standing_no_frequency` and friends satisfied |
| `one_off` | anchor, offset, lead | `null`; `grace_days` keeps the authored value (a one-off's task has a real due date) |
| `recurring` | — | as written |

Measured over the 21 imported catalog items: `recurring` 18, `standing` 2,
`one_off` 1; frequencies daily 1, weekly 3, fortnightly 1, monthly 9,
quarterly 2, semi-annual 1, annual 2.

---

## Why a clean database

`pnpm demo` loads the same fictional organisation — Ardenline Group — as a
seed. Walking the import on top of it proves nothing: the rows are already
there, and with the default `Always create new` you get two of each.

`pnpm dev` starts empty, which is also what a real deployment starts from, so
the walk above is the customer's first hour rather than a demo of a demo.

```bash
rm -rf .objectstack/data
pnpm dev
```

## What is deliberately not here

- **No import handler, action or job.** The platform's Import is the interface.
A Duly-specific import path would be a second dialect of a thing that already
works.
- **No Excel parsing.** The platform's endpoint accepts `.xlsx` itself; CSV is
what these samples ship as because it diffs.
- **No `sys_business_unit_member` rows.** Membership is what
`sys_user.primary_business_unit_id` is derived from, and it is Setup's
surface, not this walk's — `duly_duty.business_unit` is written directly from
the CSV and does not depend on it.
7 changes: 7 additions & 0 deletions samples/business-units.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name,code,kind,parent_business_unit_id,active
Ardenline Group,ARD,company,,true
Northgate Plant,NGP,division,Ardenline Group,true
Riverside Plant,RVP,division,Ardenline Group,true
Central Office,CEN,division,Ardenline Group,true
Northgate Operations,NGP-OPS,department,Northgate Plant,true
Northgate Quality,NGP-QA,department,Northgate Plant,true
22 changes: 22 additions & 0 deletions samples/catalog-items.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name,position_code,form,frequency,due_anchor,due_offset_days,lead_days,grace_days,regulation_ref,description,active
Emissions return,plant_compliance_officer,recurring,monthly,period_end,-5,10,3,Group Environment Standard GE-02 §5,"Submit the site emissions figures for the month, with the meter readings they were derived from.",true
Waste transfer log review,plant_compliance_officer,recurring,monthly,period_start,4,7,2,Group Environment Standard GE-04 §2,"Check every transfer note raised last month against the carrier register; flag anything unmatched.",true
Effluent sampling record,plant_compliance_officer,recurring,weekly,period_start,1,3,1,Site Discharge Consent DC-11 cl.4,Draw and log the weekly outfall sample. Record the result even when it is within limits.,true
Permit condition review,plant_compliance_officer,recurring,quarterly,period_end,-10,21,5,Group Environment Standard GE-09 §1,"Walk the permit conditions one by one and record, for each, the evidence that it was met this quarter.",true
Site environmental audit,plant_compliance_officer,recurring,semiannual,period_end,0,150,10,Group Assurance Plan AP-3 §6,"Full walk-round audit against the group environmental standard, with findings and owners.",true
Annual environmental statement,plant_compliance_officer,recurring,annual,period_end,-30,60,14,Group Environment Standard GE-01 §8,Compile the year's environmental performance into the statement the group publishes.,true
Keep the permit register current,plant_compliance_officer,standing,,,,,,Group Environment Standard GE-09 §4,"The register reflects the permits actually in force — no expiry passes without the entry being updated. Never ""done""; attested, not ticked.",true
Daily line start-up check,shift_supervisor,recurring,daily,period_start,0,1,0,Works Instruction WI-101 §2,Confirm the line is safe to start and record the guard and interlock checks before the first run of the day.,true
Shift handover record,shift_supervisor,recurring,weekly,period_start,0,2,0,Works Instruction WI-120 §3,"Written handover for every shift change in the week: state of the line, anything left open.",true
Line safety walk,shift_supervisor,recurring,monthly,period_start,2,7,2,Site Safety Standard SS-07 §2,Walk the line against the safety checklist with an operator present. Log what you fixed on the spot.,true
Toolbox talk record,shift_supervisor,recurring,monthly,period_start,9,7,3,Site Safety Standard SS-07 §5,Run one toolbox talk with the shift and record who attended.,true
Lifting equipment check,shift_supervisor,recurring,quarterly,period_start,5,14,5,Works Instruction WI-204 §1,"Visual check and tag review of every sling, hoist and eyebolt on the line.",true
Contractor induction refresh,shift_supervisor,recurring,annual,period_end,-60,120,21,Site Safety Standard SS-15 §3,"Re-run the site induction for every contractor still holding a pass, and retire the passes nobody claimed.",true
Answer the duty phone,shift_supervisor,standing,,,,,,Works Instruction WI-002 §1,The out-of-hours phone is carried and answered. There is no version of this that is ever finished.,true
Overtime justification summary,shift_supervisor,recurring,monthly,period_end,-2,7,3,People Policy PP-22 cl.6,One line per overtime shift worked: why it was needed and what it covered.,true
Calibration verification,quality_technician,recurring,monthly,period_start,6,7,2,Quality Manual QM-31 §4,"Verify each instrument against its reference standard and record the deviation, in range or not.",true
Retained sample review,quality_technician,recurring,fortnightly,period_start,2,5,1,Quality Manual QM-18 §2,Inspect the retained samples due for review and dispose of anything past its retention window.,true
Nonconformance log review,quality_technician,recurring,monthly,period_start,1,7,2,Quality Manual QM-05 §3,Review every nonconformance raised last month and confirm each one has an owner and a closing date.,true
Cleaning verification swabs,quality_technician,recurring,weekly,period_start,3,3,1,Quality Manual QM-22 §7,Swab the changeover points after the weekly clean and log the plate counts.,true
Instrument drift check,quality_technician,recurring,monthly,period_start,8,7,2,,Compare this month's calibration deviations against the last three and note any instrument trending out.,true
Commissioning file handover,quality_technician,one_off,,,,,7,Project Standard PS-06 §5,"Hand the commissioning file to operations: as-built drawings, test records, spares list, signed off.",false
Loading
Loading