# Suppa 1.0 → v2 code mapping

The API migration path and this code path share one authority: `classifyV1Field()` in suppa-mcp-2. Read the v1 schema with `suppa_read_v1_entities` → `suppa_read_v1_entity_props` (+ `suppa_read_v1_enum` for enum fields); normalize names with the same rules as `suppa_migrate_entities_from_v1` (entities → PascalCase, fields → camelCase, semantic dictionary, Cyrillic transliteration). Then map each classified field to a decorator with the table below. Never invent a mapping not listed here. The normalized names are a PROPOSAL: transliteration plus a small dictionary cannot translate, so `Опис причини` comes back as `descriptionPrychyny` and the plan lists it under `needs_translation`. Replace every one of those with the English meaning (`reasonDescription`) before applying the plan — the builder refuses a transliterated name anyway, and a name is far cheaper to fix before the column exists than after.

## Builder-type → decorator table

| `classifyV1Field` v2Type | Decorator to emit |
|---|---|
| `text` | `@Column({ type: FieldTypeEnum.Text })` |
| `numeric` | `@Column({ type: FieldTypeEnum.Numeric })` |
| `integer` | `@Column({ type: FieldTypeEnum.Integer })` |
| `boolean` | `@Column({ type: FieldTypeEnum.Boolean })` |
| `timestamp` | `@Column({ type: FieldTypeEnum.Timestamp })` — plus `subType: 'date'` or `subType: 'time'` when `classifyV1Field` returns that `v2SubType` (see below) |
| `json` | `@Column({ type: FieldTypeEnum.JSON })` |
| `uuid` | `@Column({ type: FieldTypeEnum.UUID })` |
| `multi-language` | `@Column({ type: FieldTypeEnum.MultiLang })` |
| `icon` | `@Column({ type: FieldTypeEnum.Icon })` — scalar by default; use `@ManyToOne(() => IconsEntity)` only when v1 reports a relation |
| `enum` | `@Enum(<Field>Enum, <Field>Titles, { … })` + a TypeScript string enum built from `suppa_read_v1_enum` values (member keys PascalCase) + a seed is NOT needed (enum values live in the decorator maps). A ported v1 enum belongs to one field, so **no global name**: add the `'<Entity>.<field>'` first argument only if a seed or a second entity references the value set by name (SKILL.md rule 17) |
| `multi-enum` | `@MultiEnum(…)` — same signature as `@Enum` |
| `file` | `@File({ title: … })` |
| `multi-file` | `@MultiFile({ title: … })` |
| `many-to-one` | `@ManyToOne(() => <TargetClass>)` |
| `many-to-many` | `@ManyToMany(() => <TargetClass>)` |
| `one-to-many` | `@OneToMany(() => <TargetClass>, <inverse>)` — **inverse is a blocking question** (v1 props don't name it). NEVER produced by `classifyV1Field` — see the warning below |
| `many-to-many-backref` | `@ManyToManyBackRef(() => <TargetClass>, <inverse>)` — **inverse is a blocking question**. NEVER produced by `classifyV1Field` |
| `serial` | never emitted — `SystemBaseEntity.id` already provides it, and `classifyV1Field` never returns it |
| kind = `skip` | emit `// SKIPPED: <field> — <note from classifyV1Field>` inside the class body — never silently drop a field |

> **This table is keyed on the CLASSIFIER'S OUTPUT (`v2Type`), never on a raw v1 `sub_type`.**
> The distinction matters most for one string: v1 reports `sub_type: 'one-to-many'` on an ordinary
> foreign-key column, and `classifyV1Field` maps that to **`many-to-one`** (`v1Fields.ts` —
> `"one-to-many": ["many-to-one", false]`). Matching the raw v1 string against the left column
> instead would emit `@OneToMany`, a back reference, where `@ManyToOne` is correct — the FK would
> never be created. The `one-to-many`, `many-to-many-backref` and `serial` rows exist only to say
> what to write IF that v2 type ever reaches you: the reverse-side decorators are authored,
> parent-side additions you add deliberately, never products of the migration.

### Never drop a field silently

If `classifyV1Field` returns `kind = 'skip'` — a system/structural field, an unsupported relation sub_type, or an unrecognized v1 type — the field must still appear in the generated class as a `// SKIPPED: <field> — <reason>` comment, using the classifier's own note as `<reason>`. Omitting it is not an option: the generated code must account for every v1 field, migrated or not.

## `v2SubType` — date-only and time-only v1 fields

`classifyV1Field` returns `v2SubType: 'date'` for a v1 date-only field and `v2SubType: 'time'` for a time-only one. Both map to a documented option: `FieldTypeEnum.Timestamp` plus that `subType`, which the platform turns into a real Postgres `date` / `time` column. There is no `FieldTypeEnum.Date` and no `FieldTypeEnum.Time`.

```ts
@Column({ name: 'birthday', type: FieldTypeEnum.Timestamp, subType: 'date' })
birthday: string;

@Column({ name: 'opensAt', type: FieldTypeEnum.Timestamp, subType: 'time' })
opensAt: string;
```

Get it right the first time: `subType` is immutable after creation except for the `'encrypted'` toggle, so a field created as plain `timestamp` cannot later be converted to `date` — it has to be replaced by a new field. Never emit `subType` on an `@Enum`/`@MultiEnum` field; the decorator writes it.

## Documented open edges

- Icon duality: default scalar `FieldTypeEnum.Icon`; relation form only if v1 reports `type: relation` targeting icons.
- Lossy mappings carry the classifier's note as a trailing comment, e.g. `// v1 html_text → text (2.0 has no rich-text type)`.

## Worked example

v1 entity `zakazy` (fields: `nazva` — string/string, `status` — custom_enum/one-to-many, `klient` — relation/one-to-many).

Name normalization (per `nameMap.ts`'s `SEMANTIC_DICT`): `zakazy` → `Orders` (entity), `nazva` → `Name` → field `name`, `klient` → `Customer` → field `customer`, targeting entity class `Customers` (the v1 `klient` relation points at the customers entity, whose own v1 name normalizes to `Customers`).

Classification (per `classifyV1Field`): `nazva` (`string`/`string`) → scalar `text`. `status` (`custom_enum`/`one-to-many`) → `kind: enum`, `v2Type: 'enum'`, not multiple (sub_type is `one-to-many`, not `many-to-many`). `klient` (`relation`/`one-to-many`) → `kind: relation`, `v2Type: 'many-to-one'`, not multiple.

```ts
import { Column, Entity, Enum, FieldTypeEnum, ManyToOne, SystemBaseEntity } from '@suppa/sdk';
import { Customers } from './customers.entity';

export enum StatusEnum {
	New = 'new',
	Completed = 'completed',
}

export const StatusTitles = {
	// Illustrative only — read the real value set with `suppa_read_v1_enum`.
	[StatusEnum.New]: { title: { en: 'New' } },
	[StatusEnum.Completed]: { title: { en: 'Completed' } },
};

@Entity({
	name: 'Orders',
	title: { en: 'zakazy' }, // carries the original v1 entity name through
	// BLOCKING QUESTION: v1 does not report a business key for this entity —
	// confirm the correct importKeyFields with the user before migrating.
})
export class Orders extends SystemBaseEntity {
	@Column({ name: 'name', type: FieldTypeEnum.Text })
	name: string;

	// No global name: nothing outside this field references the value set.
	@Enum(StatusEnum, StatusTitles)
	status: StatusEnum;

	// Nullable (no `nullable: false`), so the property is optional.
	@ManyToOne(() => Customers)
	customer?: Customers;
}
```
