# Table Modifiers

Table modifiers transform field values as they are stored in and retrieved from the database. They enable transparent encryption, hashing, localization, and custom data transformations. Add modifiers to a table class using the `Table.with()` method.

## Built-in Modifiers

### EncryptionModifier

The `EncryptionModifier` encrypts field values before storing them and decrypts them on retrieval. Both operations happen transparently.

```typescript
import { Table, Field, EncryptionModifier, Encrypted } from "@antelopejs/interface-database-decorators";

class UserCredentials extends Table.with(EncryptionModifier) {
  @Encrypted({
    secretKey: process.env.ENCRYPTION_KEY || "default-key",
    algorithm: "aes-256-gcm",
    ivSize: 16,
  })
  @Field("string")
  declare creditCardNumber: string;

  @Encrypted({ secretKey: process.env.ENCRYPTION_KEY || "default-key" })
  @Field("any")
  declare personalData: {
    ssn: string;
    birthDate: string;
  };
}
```

#### How It Works

- Data is encrypted using the specified algorithm before database insertion.
- A random initialization vector (IV) is generated for each write.
- For authenticated encryption algorithms (e.g., `aes-256-gcm`), an auth tag is stored alongside the ciphertext.
- On retrieval, the data is automatically decrypted.
- Date objects are preserved through JSON serialization with a custom encoder.

#### Options

| Option      | Type     | Default           | Description                      |
| ----------- | -------- | ----------------- | -------------------------------- |
| `secretKey` | `string` | (required)        | The encryption key.              |
| `algorithm` | `string` | `"aes-256-gcm"`   | Encryption algorithm.            |
| `ivSize`    | `number` | `16`              | Initialization vector size in bytes. |

### HashModifier

The `HashModifier` stores a one-way hash of field values. Unlike encryption, hashing is irreversible -- you cannot retrieve the original value. Use this for passwords and other sensitive data that only needs equality verification.

```typescript
import { Table, Field, HashModifier, Hashed } from "@antelopejs/interface-database-decorators";

class User extends Table.with(HashModifier) {
  @Field("string")
  declare email: string;

  @Hashed({ algorithm: "sha256" })
  @Field("string")
  declare password: string;

  verifyPassword(plainPassword: string): boolean {
    return this.testHash("password", plainPassword);
  }
}
```

#### How It Works

- A random salt is generated and stored in the field's metadata on first write.
- The value is hashed with the salt using the specified algorithm.
- The `testHash` method (added by the mixin) hashes a candidate value with the stored salt and compares the result.

#### Options

| Option      | Type     | Default    | Description        |
| ----------- | -------- | ---------- | ------------------ |
| `algorithm` | `string` | `"sha256"` | Hashing algorithm. |

#### Mixin Method

The `HashModifier` mixin adds a `testHash` method to table instances:

```typescript
testHash(field: string, value: unknown): boolean
```

Returns `true` if `value`, when hashed, matches the stored hash for the given field.

### LocalizationModifier

The `LocalizationModifier` stores multiple language versions of a field value. Access a specific locale by calling `localize()` on the table instance.

```typescript
import { Table, Field, LocalizationModifier, Localized } from "@antelopejs/interface-database-decorators";

class Product extends Table.with(LocalizationModifier) {
  @Field("number")
  declare price: number;

  @Localized({ fallbackLocale: "en" })
  @Field("string")
  declare name: string;

  @Localized({ fallbackLocale: "en" })
  @Field("string")
  declare description: string;
}
```

#### Usage

```typescript
const product = new Product();
product._id = "prod-123";
product.price = 99.99;

// Set localized content
product.localize("en").name = "Premium Headphones";
product.localize("fr").name = "Casque Premium";
product.localize("es").name = "Auriculares Premium";

// Read localized content
const spanish = product.localize("es");
console.log(spanish.name); // "Auriculares Premium"

// Fallback to default locale when translation is missing
const german = product.localize("de");
console.log(german.name); // "Premium Headphones" (fallback)
```

#### Wildcard Key

The wildcard key `"*"` retrieves or sets all translations at once:

```typescript
// Get all translations
const allNames = product.localize("*").name;
// { en: "Premium Headphones", fr: "Casque Premium", es: "Auriculares Premium" }

// Set all translations
product.localize("*").name = {
  en: "Premium Headphones",
  fr: "Casque Premium",
  es: "Auriculares Premium",
  de: "Premium-Kopfhoerer",
};
```

#### Options

| Option           | Type     | Description                                           |
| ---------------- | -------- | ----------------------------------------------------- |
| `fallbackLocale` | `string` | Locale to use when the requested locale is unavailable. |

#### Mixin Method

The `LocalizationModifier` mixin adds a `localize` method:

```typescript
localize(locale: string, fields?: Array<keyof this>): this
```

Sets the active locale for accessing localized fields. If `fields` is provided, only those fields are unlocked for the specified locale.

### AutoDateModifier

The `AutoDateModifier` automatically populates timestamp fields during insert and update operations. Use the `CreationTime` and `UpdateTime` decorators to mark fields.

```typescript
import { Table, CreationTime, UpdateTime } from "@antelopejs/interface-database-decorators";

class Article extends Table {
  declare title: string;

  @CreationTime()
  declare creationDate: Date;

  @UpdateTime()
  declare updateDate: Date;
}
```

#### How It Works

- On insert, both `CreationTime` and `UpdateTime` fields are set to the current date.
- On update, `UpdateTime` fields are refreshed while `CreationTime` fields are removed from the payload so the original creation date is preserved.
- This is an event-only modifier: it does not transform stored values and does not require adding a mixin through `Table.with()`.

## Combine Multiple Modifiers

Pass multiple modifier classes to `Table.with()`:

```typescript
import {
  Table,
  Field,
  EncryptionModifier,
  HashModifier,
  LocalizationModifier,
  Encrypted,
  Hashed,
  Localized,
} from "@antelopejs/interface-database-decorators";

class UserProfile extends Table.with(EncryptionModifier, HashModifier, LocalizationModifier) {
  @Encrypted({ secretKey: process.env.SECRET_KEY || "default-key" })
  @Field("any")
  declare privateInfo: {
    address: string;
    phoneNumber: string;
  };

  @Hashed()
  @Field("string")
  declare password: string;

  @Localized({ fallbackLocale: "en" })
  @Field("string")
  declare bio: string;
}
```

## Create Custom Modifiers

### OneWayModifier

A `OneWayModifier` transforms data in one direction (when writing to the database). Use it for irreversible transformations like hashing.

```typescript
import { OneWayModifier, MixinSymbol, attachModifier } from "@antelopejs/interface-database-decorators";
import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators";
import { createHash } from "crypto";

class PrefixHashModifier extends OneWayModifier<string | undefined, [], {}, { algorithm?: string }> {
  public readonly autolock = true;

  public override lock(_locked: string | undefined, value: unknown) {
    if (value === undefined) return undefined;
    return createHash(this.options.algorithm || "sha256")
      .update(JSON.stringify(value))
      .digest("hex");
  }

  [MixinSymbol] = class {};
}

const PrefixHashed = MakePropertyDecorator(
  (target, propertyKey, options?: { algorithm?: string }) => {
    attachModifier(target.constructor, PrefixHashModifier, propertyKey, options || {});
  },
);
```

The generic parameters for `OneWayModifier<LockedType, Args, Meta, Options>` are:

| Parameter    | Description                                         |
| ------------ | --------------------------------------------------- |
| `LockedType` | The type stored in the database after transformation |
| `Args`       | Additional arguments for `lock` and `test`           |
| `Meta`       | Per-field metadata stored alongside the value        |
| `Options`    | Configuration passed via the decorator               |

### TwoWayModifier

A `TwoWayModifier` extends `OneWayModifier` with an `unlock` method for reversible transformations like encryption:

```typescript
import { TwoWayModifier, MixinSymbol, attachModifier } from "@antelopejs/interface-database-decorators";
import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators";

class Base64Modifier extends TwoWayModifier<string, [], {}, { prefix?: string }> {
  public readonly autolock = true;
  public readonly autounlock = true;

  public override lock(_locked: string | undefined, value: unknown) {
    if (value === undefined) return undefined;
    const prefix = this.options.prefix || "b64-";
    return `${prefix}${Buffer.from(JSON.stringify(value)).toString("base64")}`;
  }

  public override unlock(locked: string) {
    const prefix = this.options.prefix || "b64-";
    const encoded = locked.substring(prefix.length);
    return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
  }

  public override unlockrequest(data: any) {
    return data;
  }

  [MixinSymbol] = class {};
}

const Base64Encoded = MakePropertyDecorator(
  (target, propertyKey, options?: { prefix?: string }) => {
    attachModifier(target.constructor, Base64Modifier, propertyKey, options || {});
  },
);
```

### ContainerModifier

For modifiers that store multiple values in a key-value structure (like `LocalizationModifier`), extend `ContainerModifier`. It provides built-in support for the wildcard key `"*"` for bulk operations:

```typescript
import { ContainerModifier } from "@antelopejs/interface-database-decorators";

class MyContainerModifier extends ContainerModifier<{}, { fallback?: string }> {
  // Override unlock() to add fallback behavior
  // Override unlockrequest() for query-time data access
}
```

## Modifier Events

Modifiers can hook into the data lifecycle by implementing event handler methods. These methods are called during data conversion and database operations:

| Event           | Triggered When                            |
| --------------- | ----------------------------------------- |
| `fromPlainData` | After converting plain data to an instance |
| `toPlainData`   | After converting an instance to plain data |
| `fromDatabase`  | After reading from the database            |
| `toDatabase`    | Before writing to the database             |
| `insert`        | Before inserting a new record              |
| `update`        | Before updating an existing record         |

```typescript
import { Modifier, MixinSymbol } from "@antelopejs/interface-database-decorators";

class TimestampModifier extends Modifier {
  public insert(instance: any, field: string) {
    if (field === "createdAt" && !instance[field]) {
      instance[field] = new Date();
    }
  }

  public update(instance: any, field: string) {
    if (field === "updatedAt") {
      instance[field] = new Date();
    }
  }

  [MixinSymbol] = class {};
}
```

Event-only modifiers (without `lock`/`unlock`) are valid and useful for side effects like logging, validation, or auto-populating fields.
