Pre-Save Operations
Pre-save operations are transformations applied to your data before it's written to the database. They provide a powerful way to handle encryption, hashing, validation, sanitization, and custom transformations directly in your database operations.
Overview
Pre-save operations support:
- Encryption - AES-256 encryption for sensitive data
- Hashing - One-way hashing for passwords (bcrypt, argon2, SHA)
- Masking - Mask sensitive data (credit cards, SSN, emails)
- Validation - Validate data before insert
- Transformation - Custom transformations
- Sanitization - Remove HTML/script tags
- Normalization - Normalize emails, phone numbers
- Computed Fields - Generate slugs, UUIDs, timestamps
Basic Usage
Insert with Pre-Save Operations
- TypeScript
- Java
- Go
- .NET
import { PreSave } from '@ductape/sdk';
// Insert a user with password hashing and email normalization
const result = await ductape.databases.insert({
table: 'users',
data: {
name: 'John Doe',
email: 'John.Doe@Example.COM',
password: 'secret123',
},
preSave: [
PreSave.hash('password', { algorithm: 'bcrypt', saltRounds: 12 }),
PreSave.normalizeEmail('email'),
PreSave.trim('name'),
],
returning: true,
});
// Result: password is hashed, email is "john.doe@example.com"
import Map.of( PreSave ) from '@ductape/sdk';
// Insert a user with password hashing and email normalization
Map<String, Object> result = ductape.databases().insert(Map<String, Object>.of(
"table", "users",
data: Map.of(
"name", "John Doe",
"email", "John.Doe@Example.COM",
"password", "secret123"
),
preSave: [
PreSave.hash('password', Map.of( "algorithm", "bcrypt", "saltRounds", 12 )),
PreSave.normalizeEmail('email'),
PreSave.trim('name'),
],
"returning", true
));
// Result: password is hashed, email is "john.doe@example.com"
import "context"
import { PreSave } from '@ductape/sdk';
// Insert a user with password hashing and email normalization
result := client.Databases.Insert(ctx, map[string]any{
"table": "users",
data: {
"name": "John Doe",
"email": "John.Doe@Example.COM",
"password": "secret123",
},
preSave: [
PreSave.hash('password', { "algorithm": "bcrypt", "saltRounds": 12 }),
PreSave.normalizeEmail('email'),
PreSave.trim('name'),
],
"returning": true,
});
// Result: password is hashed, email is "john.doe@example.com"
import { PreSave } from '@ductape/sdk';
// Insert a user with password hashing and email normalization
var result = await ductape.Database.Insert(new Dictionary<string, object?>
{
["table"] = "users",
data: {
["name"] = "John Doe",
["email"] = "John.Doe@Example.COM",
["password"] = "secret123",
},
preSave: [
PreSave.hash('password', { ["algorithm"] = "bcrypt", ["saltRounds"] = 12 }),
PreSave.normalizeEmail('email'),
PreSave.trim('name'),
],
["returning"] = true,
});
// Result: password is hashed, email is "john.doe@example.com"
Update with Pre-Save Operations
- TypeScript
- Java
- Go
- .NET
await ductape.databases.update({
table: 'users',
data: {
password: 'newpassword',
bio: '<script>alert("xss")</script>My bio',
},
where: { id: userId },
preSave: [
PreSave.hash('password'),
PreSave.sanitize('bio'),
],
});
ductape.databases.update(Map.of(
"table", "users",
data: Map.of(
"password", "newpassword",
"bio", "<script>alert("xss")</script>My bio"
),
where: Map.of( id: userId ),
preSave: [
PreSave.hash('password'),
PreSave.sanitize('bio'),
]
));
client.databases.update({
"table": "users",
data: {
"password": "newpassword",
"bio": "<script>alert("xss")</script>My bio",
},
where: { id: userId },
preSave: [
PreSave.hash('password'),
PreSave.sanitize('bio'),
],
});
await ductape.databases.update({
["table"] = "users",
data: {
["password"] = "newpassword",
["bio"] = "<script>alert("xss")</script>My bio",
},
where: { id: userId },
preSave: [
PreSave.hash('password'),
PreSave.sanitize('bio'),
],
});
Available Operations
Encryption
Encrypt sensitive data using AES-256. The encrypted value can be decrypted later.
- TypeScript
- Java
- Go
- .NET
// Encrypt a field
await ductape.databases.insert({
table: 'secrets',
data: {
api_key: 'sk_live_abc123',
user_id: 1,
},
preSave: [
PreSave.encrypt('api_key', {
key: process.env.ENCRYPTION_KEY,
algorithm: 'aes-256-gcm', // default
}),
],
preSaveConfig: {
encryptionKey: process.env.ENCRYPTION_KEY, // Or set globally
},
});
// Encrypt a field
ductape.databases().insert(Map<String, Object>.of(
"table", "secrets",
data: Map.of(
"api_key", "sk_live_abc123",
"user_id", 1
),
preSave: [
PreSave.encrypt('api_key', Map.of(
key: System.getenv("ENCRYPTION_KEY"),
"algorithm", "aes-256-gcm", // default
)),
],
preSaveConfig: Map.of(
encryptionKey: System.getenv("ENCRYPTION_KEY"), // Or set globally
)
));
import "context"
// Encrypt a field
client.Databases.Insert(ctx, map[string]any{
"table": "secrets",
data: {
"api_key": "sk_live_abc123",
"user_id": 1,
},
preSave: [
PreSave.encrypt('api_key', {
key: os.Getenv("ENCRYPTION_KEY"),
"algorithm": "aes-256-gcm", // default
}),
],
preSaveConfig: {
encryptionKey: os.Getenv("ENCRYPTION_KEY"), // Or set globally
},
});
// Encrypt a field
await ductape.Database.Insert(new Dictionary<string, object?>
{
["table"] = "secrets",
data: {
["api_key"] = "sk_live_abc123",
["user_id"] = 1,
},
preSave: [
PreSave.encrypt('api_key', {
key: Environment.GetEnvironmentVariable("ENCRYPTION_KEY"),
["algorithm"] = "aes-256-gcm", // default
}),
],
preSaveConfig: {
encryptionKey: Environment.GetEnvironmentVariable("ENCRYPTION_KEY"), // Or set globally
},
});
Decrypting data:
- TypeScript
- Java
- Go
- .NET
import { decrypt } from '@ductape/sdk';
const result = await ductape.databases.query({
table: 'secrets',
where: { user_id: 1 },
});
const decryptedKey = decrypt(result.data[0].api_key, {
key: process.env.ENCRYPTION_KEY,
});
import Map.of( decrypt ) from '@ductape/sdk';
Map<String, Object> result = ductape.databases.query(Map.of(
"table", "secrets",
where: Map.of( "user_id", 1 )
));
Map<String, Object> decryptedKey = decrypt(result.data[0].api_key, Map.of(
key: System.getenv("ENCRYPTION_KEY")
));
import { decrypt } from '@ductape/sdk';
result := client.databases.query({
"table": "secrets",
where: { "user_id": 1 },
});
decryptedKey := decrypt(result.data[0].api_key, {
key: os.Getenv("ENCRYPTION_KEY"),
});
import { decrypt } from '@ductape/sdk';
var result = await ductape.databases.query({
["table"] = "secrets",
where: { ["user_id"] = 1 },
});
var decryptedKey = decrypt(result.data[0].api_key, {
key: Environment.GetEnvironmentVariable("ENCRYPTION_KEY"),
});
Hashing (Passwords)
One-way hashing for passwords and sensitive data that doesn't need to be decrypted.
- TypeScript
- Java
- Go
- .NET
// Hash with bcrypt (recommended for passwords)
PreSave.hash('password', { algorithm: 'bcrypt', saltRounds: 12 })
// Hash with SHA-256 (for checksums, etc.)
PreSave.hash('checksum', { algorithm: 'sha256' })
// Available algorithms: 'bcrypt', 'argon2', 'sha256', 'sha512', 'md5'
// Hash with bcrypt (recommended for passwords)
PreSave.hash('password', Map.of( "algorithm", "bcrypt", "saltRounds", 12 ))
// Hash with SHA-256 (for checksums, etc.)
PreSave.hash('checksum', Map.of( "algorithm", "sha256" ))
// Available "algorithms", "bcrypt", 'argon2', 'sha256', 'sha512', 'md5'
// Hash with bcrypt (recommended for passwords)
PreSave.hash('password', { "algorithm": "bcrypt", "saltRounds": 12 })
// Hash with SHA-256 (for checksums, etc.)
PreSave.hash('checksum', { "algorithm": "sha256" })
// Available "algorithms": "bcrypt", 'argon2', 'sha256', 'sha512', 'md5'
// Hash with bcrypt (recommended for passwords)
PreSave.hash('password', { ["algorithm"] = "bcrypt", ["saltRounds"] = 12 })
// Hash with SHA-256 (for checksums, etc.)
PreSave.hash('checksum', { ["algorithm"] = "sha256" })
// Available ["algorithms"] = "bcrypt", 'argon2', 'sha256', 'sha512', 'md5'
Verifying hashed passwords:
- TypeScript
- Java
- Go
- .NET
import { verifyHash } from '@ductape/sdk';
const user = await ductape.databases.query({
table: 'users',
where: { email: 'john@example.com' },
});
const isValid = verifyHash('user_password', user.data[0].password);
if (isValid) {
// Password matches
}
import Map.of( verifyHash ) from '@ductape/sdk';
Map<String, Object> user = ductape.databases.query(Map.of(
"table", "users",
where: Map.of( "email", "john@example.com" )
));
Map<String, Object> isValid = verifyHash('user_password', user.data[0].password);
if (isValid) Map.of(
// Password matches
)
import { verifyHash } from '@ductape/sdk';
user := client.databases.query({
"table": "users",
where: { "email": "john@example.com" },
});
isValid := verifyHash('user_password', user.data[0].password);
if (isValid) {
// Password matches
}
import { verifyHash } from '@ductape/sdk';
var user = await ductape.databases.query({
["table"] = "users",
where: { ["email"] = "john@example.com" },
});
var isValid = verifyHash('user_password', user.data[0].password);
if (isValid) {
// Password matches
}
Masking
Mask sensitive data while preserving partial visibility.
- TypeScript
- Java
- Go
- .NET
// Credit card: **** **** **** 1234
PreSave.mask('card_number', 'credit-card')
// SSN: ***-**-1234
PreSave.mask('ssn', 'ssn')
// Phone: ******1234
PreSave.mask('phone', 'phone')
// Email: j***@example.com
PreSave.mask('email', 'email')
// Custom masking
PreSave.mask('account', 'custom', {
showFirst: 2,
showLast: 4,
maskChar: 'X',
})
// "AB12345678" → "ABXXXX5678"
// Credit card: **** **** **** 1234
PreSave.mask('card_number', 'credit-card')
// SSN: ***-**-1234
PreSave.mask('ssn', 'ssn')
// Phone: ******1234
PreSave.mask('phone', 'phone')
// Email: j***@example.com
PreSave.mask('email', 'email')
// Custom masking
PreSave.mask('account', 'custom', Map.of(
"showFirst", 2,
"showLast", 4,
"maskChar", "X"
))
// "AB12345678" → "ABXXXX5678"
// Credit card: **** **** **** 1234
PreSave.mask('card_number', 'credit-card')
// SSN: ***-**-1234
PreSave.mask('ssn', 'ssn')
// Phone: ******1234
PreSave.mask('phone', 'phone')
// Email: j***@example.com
PreSave.mask('email', 'email')
// Custom masking
PreSave.mask('account', 'custom', {
"showFirst": 2,
"showLast": 4,
"maskChar": "X",
})
// "AB12345678" → "ABXXXX5678"
// Credit card: **** **** **** 1234
PreSave.mask('card_number', 'credit-card')
// SSN: ***-**-1234
PreSave.mask('ssn', 'ssn')
// Phone: ******1234
PreSave.mask('phone', 'phone')
// Email: j***@example.com
PreSave.mask('email', 'email')
// Custom masking
PreSave.mask('account', 'custom', {
["showFirst"] = 2,
["showLast"] = 4,
["maskChar"] = "X",
})
// "AB12345678" → "ABXXXX5678"
Store original value:
- TypeScript
- Java
- Go
- .NET
PreSave.mask('card_number', 'credit-card', {
storeOriginalIn: 'card_number_encrypted',
})
// Original goes to card_number_encrypted (encrypt it too!)
PreSave.mask('card_number', 'credit-card', Map.of(
"storeOriginalIn", "card_number_encrypted"
))
// Original goes to card_number_encrypted (encrypt it too!)
PreSave.mask('card_number', 'credit-card', {
"storeOriginalIn": "card_number_encrypted",
})
// Original goes to card_number_encrypted (encrypt it too!)
PreSave.mask('card_number', 'credit-card', {
["storeOriginalIn"] = "card_number_encrypted",
})
// Original goes to card_number_encrypted (encrypt it too!)
Validation
Validate data before it's written to the database.
- TypeScript
- Java
- Go
- .NET
await ductape.databases.insert({
table: 'users',
data: {
email: 'invalid-email',
age: 15,
role: 'superadmin',
},
preSave: [
PreSave.validate('email', [
{ type: 'required' },
{ type: 'email', message: 'Please enter a valid email' },
]),
PreSave.validate('age', [
{ type: 'min', value: 18, message: 'Must be 18 or older' },
{ type: 'max', value: 120 },
]),
PreSave.validate('role', [
{ type: 'enum', value: ['user', 'admin', 'moderator'] },
]),
],
});
// Throws: "Validation failed: email: Please enter a valid email, age: Must be 18 or older"
ductape.databases().insert(Map<String, Object>.of(
"table", "users",
data: Map.of(
"email", "invalid-email",
"age", 15,
"role", "superadmin"
),
preSave: [
PreSave.validate('email', [
Map.of( "type", "required" ),
Map.of( "type", "email", "message", "Please enter a valid email" ),
]),
PreSave.validate('age', [
Map.of( "type", "min", "value", 18, "message", "Must be 18 or older" ),
Map.of( "type", "max", "value", 120 ),
]),
PreSave.validate('role', [
Map.of( "type", "enum", value: ['user', 'admin', 'moderator'] ),
]),
]
));
// "Throws", "Validation failed: email: Please enter a valid email, age: Must be 18 or older"
import "context"
client.Databases.Insert(ctx, map[string]any{
"table": "users",
data: {
"email": "invalid-email",
"age": 15,
"role": "superadmin",
},
preSave: [
PreSave.validate('email', [
{ "type": "required" },
{ "type": "email", "message": "Please enter a valid email" },
]),
PreSave.validate('age', [
{ "type": "min", "value": 18, "message": "Must be 18 or older" },
{ "type": "max", "value": 120 },
]),
PreSave.validate('role', [
{ "type": "enum", value: ['user', 'admin', 'moderator'] },
]),
],
});
// "Throws": "Validation failed: email: Please enter a valid email, age: Must be 18 or older"
await ductape.Database.Insert(new Dictionary<string, object?>
{
["table"] = "users",
data: {
["email"] = "invalid-email",
["age"] = 15,
["role"] = "superadmin",
},
preSave: [
PreSave.validate('email', [
{ ["type"] = "required" },
{ ["type"] = "email", ["message"] = "Please enter a valid email" },
]),
PreSave.validate('age', [
{ ["type"] = "min", ["value"] = 18, ["message"] = "Must be 18 or older" },
{ ["type"] = "max", ["value"] = 120 },
]),
PreSave.validate('role', [
{ ["type"] = "enum", value: ['user', 'admin', 'moderator'] },
]),
],
});
// ["Throws"] = "Validation failed: email: Please enter a valid email, age: Must be 18 or older"
Available validation rules:
| Rule | Value | Description |
|---|---|---|
required | - | Field must not be null/empty |
email | - | Valid email format |
url | - | Valid URL format |
minLength | number | Minimum string length |
maxLength | number | Maximum string length |
min | number | Minimum numeric value |
max | number | Maximum numeric value |
pattern | regex | Match regex pattern |
enum | array | Value must be in array |
type | string | JavaScript type ('string', 'number', etc.) |
integer | - | Must be an integer |
positive | - | Must be positive number |
negative | - | Must be negative number |
custom | function | Custom validation function |
Custom validation:
- TypeScript
- Java
- Go
- .NET
PreSave.validate('username', [
{
type: 'custom',
validator: (value, record) => {
return /^[a-z0-9_]+$/.test(value) && !value.startsWith('admin');
},
message: 'Username must be lowercase alphanumeric and cannot start with "admin"',
},
])
PreSave.validate('username', [
Map.of(
"type", "custom",
validator: (value, record) => Map.of(
return /^[a-z0-9_]+$/.test(value) && !value.startsWith('admin');
),
"message", "Username must be lowercase alphanumeric and cannot start with "admin""
),
])
PreSave.validate('username', [
{
"type": "custom",
validator: (value, record) => {
return /^[a-z0-9_]+$/.test(value) && !value.startsWith('admin');
},
"message": "Username must be lowercase alphanumeric and cannot start with "admin"",
},
])
PreSave.validate('username', [
{
["type"] = "custom",
validator: (value, record) => {
return /^[a-z0-9_]+$/.test(value) && !value.startsWith('admin');
},
["message"] = "Username must be lowercase alphanumeric and cannot start with "admin"",
},
])
String Operations
- TypeScript
- Java
- Go
- .NET
// Trim whitespace
PreSave.trim('name')
// Convert to lowercase
PreSave.lowercase('email')
// Convert to uppercase
PreSave.uppercase('country_code')
// Sanitize HTML (remove script tags, etc.)
PreSave.sanitize('bio')
// Truncate to max length
PreSave.truncate('description', 200, {
suffix: '...',
wordBoundary: true
})
// Trim whitespace
PreSave.trim('name')
// Convert to lowercase
PreSave.lowercase('email')
// Convert to uppercase
PreSave.uppercase('country_code')
// Sanitize HTML (remove script tags, etc.)
PreSave.sanitize('bio')
// Truncate to max length
PreSave.truncate('description', 200, Map.of(
"suffix", "...",
"wordBoundary", true
))
// Trim whitespace
PreSave.trim('name')
// Convert to lowercase
PreSave.lowercase('email')
// Convert to uppercase
PreSave.uppercase('country_code')
// Sanitize HTML (remove script tags, etc.)
PreSave.sanitize('bio')
// Truncate to max length
PreSave.truncate('description', 200, {
"suffix": "...",
"wordBoundary": true
})
// Trim whitespace
PreSave.trim('name')
// Convert to lowercase
PreSave.lowercase('email')
// Convert to uppercase
PreSave.uppercase('country_code')
// Sanitize HTML (remove script tags, etc.)
PreSave.sanitize('bio')
// Truncate to max length
PreSave.truncate('description', 200, {
["suffix"] = "...",
["wordBoundary"] = true
})
Normalization
- TypeScript
- Java
- Go
- .NET
// Normalize email (lowercase, trim)
PreSave.normalizeEmail('email')
// " John.Doe@EXAMPLE.COM " → "john.doe@example.com"
// Normalize phone (remove formatting)
PreSave.normalizePhone('phone')
// "(555) 123-4567" → "5551234567"
// "+1 (555) 123-4567" → "+15551234567"
// Normalize email (lowercase, trim)
PreSave.normalizeEmail('email')
// " John.Doe@EXAMPLE.COM " → "john.doe@example.com"
// Normalize phone (remove formatting)
PreSave.normalizePhone('phone')
// "(555) 123-4567" → "5551234567"
// "+1 (555) 123-4567" → "+15551234567"
// Normalize email (lowercase, trim)
PreSave.normalizeEmail('email')
// " John.Doe@EXAMPLE.COM " → "john.doe@example.com"
// Normalize phone (remove formatting)
PreSave.normalizePhone('phone')
// "(555) 123-4567" → "5551234567"
// "+1 (555) 123-4567" → "+15551234567"
// Normalize email (lowercase, trim)
PreSave.normalizeEmail('email')
// " John.Doe@EXAMPLE.COM " → "john.doe@example.com"
// Normalize phone (remove formatting)
PreSave.normalizePhone('phone')
// "(555) 123-4567" → "5551234567"
// "+1 (555) 123-4567" → "+15551234567"
Number Operations
- TypeScript
- Java
- Go
- .NET
// Round to decimal places
PreSave.round('price', 2) // 19.999 → 20.00
PreSave.round('score', 0, 'floor') // 4.9 → 4
PreSave.round('rating', 1, 'ceil') // 4.01 → 4.1
// Clamp to range
PreSave.clamp('quantity', 0, 100) // -5 → 0, 150 → 100
// Round to decimal places
PreSave.round('price', 2) // 19.999 → 20.00
PreSave.round('score', 0, 'floor') // 4.9 → 4
PreSave.round('rating', 1, 'ceil') // 4.01 → 4.1
// Clamp to range
PreSave.clamp('quantity', 0, 100) // -5 → 0, 150 → 100
// Round to decimal places
PreSave.round('price', 2) // 19.999 → 20.00
PreSave.round('score', 0, 'floor') // 4.9 → 4
PreSave.round('rating', 1, 'ceil') // 4.01 → 4.1
// Clamp to range
PreSave.clamp('quantity', 0, 100) // -5 → 0, 150 → 100
// Round to decimal places
PreSave.round('price', 2) // 19.999 → 20.00
PreSave.round('score', 0, 'floor') // 4.9 → 4
PreSave.round('rating', 1, 'ceil') // 4.01 → 4.1
// Clamp to range
PreSave.clamp('quantity', 0, 100) // -5 → 0, 150 → 100
Generated Fields
- TypeScript
- Java
- Go
- .NET
// Generate UUID
PreSave.uuid('id')
// Generate timestamp
PreSave.timestamp('created_at')
// Generate slug from another field
PreSave.slug('slug', 'title', {
separator: '-',
lowercase: true,
maxLength: 50,
})
// "Hello World! This is Amazing" → "hello-world-this-is-amazing"
// Generate UUID
PreSave.uuid('id')
// Generate timestamp
PreSave.timestamp('created_at')
// Generate slug from another field
PreSave.slug('slug', 'title', Map.of(
"separator", "-",
"lowercase", true,
"maxLength", 50
))
// "Hello World! This is Amazing" → "hello-world-this-is-amazing"
// Generate UUID
PreSave.uuid('id')
// Generate timestamp
PreSave.timestamp('created_at')
// Generate slug from another field
PreSave.slug('slug', 'title', {
"separator": "-",
"lowercase": true,
"maxLength": 50,
})
// "Hello World! This is Amazing" → "hello-world-this-is-amazing"
// Generate UUID
PreSave.uuid('id')
// Generate timestamp
PreSave.timestamp('created_at')
// Generate slug from another field
PreSave.slug('slug', 'title', {
["separator"] = "-",
["lowercase"] = true,
["maxLength"] = 50,
})
// "Hello World! This is Amazing" → "hello-world-this-is-amazing"
Custom Transformations
- TypeScript
- Java
- Go
- .NET
// Simple transformation
PreSave.transform('tags', (value) => {
return value.split(',').map(t => t.trim().toLowerCase());
})
// With access to full record
PreSave.transform('full_name', (value, record) => {
return `${record.first_name} ${record.last_name}`;
})
// Computed field from other fields
PreSave.compute('total', (record) => {
return record.quantity * record.unit_price;
})
// Simple transformation
PreSave.transform('tags', (value) => Map.of(
return value.split(',').map(t => t.trim().toLowerCase());
))
// With access to full record
PreSave.transform('full_name', (value, record) => Map.of(
return `$Map.of(record.first_name) $Map.of(record.last_name)`;
))
// Computed field from other fields
PreSave.compute('total', (record) => Map.of(
return record.quantity * record.unit_price;
))
// Simple transformation
PreSave.transform('tags', (value) => {
return value.split(',').map(t => t.trim().toLowerCase());
})
// With access to full record
PreSave.transform('full_name', (value, record) => {
return `${record.first_name} ${record.last_name}`;
})
// Computed field from other fields
PreSave.compute('total', (record) => {
return record.quantity * record.unit_price;
})
// Simple transformation
PreSave.transform('tags', (value) => {
return value.split(',').map(t => t.trim().toLowerCase());
})
// With access to full record
PreSave.transform('full_name', (value, record) => {
return `${record.first_name} ${record.last_name}`;
})
// Computed field from other fields
PreSave.compute('total', (record) => {
return record.quantity * record.unit_price;
})
Default Values
- TypeScript
- Java
- Go
- .NET
// Static default
PreSave.default('status', 'pending')
// Dynamic default (using generator function)
{
field: 'expires_at',
type: PreSaveOperationType.DEFAULT,
generator: () => new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
}
// Static default
PreSave.default('status', 'pending')
// Dynamic default (using generator function)
Map.of(
"field", "expires_at",
type: PreSaveOperationType.DEFAULT,
generator: () => new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
)
// Static default
PreSave.default('status', 'pending')
// Dynamic default (using generator function)
{
"field": "expires_at",
type: PreSaveOperationType.DEFAULT,
generator: () => new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
}
// Static default
PreSave.default('status', 'pending')
// Dynamic default (using generator function)
{
["field"] = "expires_at",
type: PreSaveOperationType.DEFAULT,
generator: () => new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
}
Configuration
Global Configuration
Set default configuration for all pre-save operations:
- TypeScript
- Java
- Go
- .NET
await ductape.databases.insert({
table: 'users',
data: userData,
preSave: [...],
preSaveConfig: {
encryptionKey: process.env.ENCRYPTION_KEY,
defaultHashAlgorithm: 'bcrypt',
defaultSaltRounds: 12,
throwOnValidationError: true, // default
},
});
ductape.databases().insert(Map<String, Object>.of(
"table", "users",
data: userData,
preSave: [...],
preSaveConfig: Map.of(
encryptionKey: System.getenv("ENCRYPTION_KEY"),
"defaultHashAlgorithm", "bcrypt",
"defaultSaltRounds", 12,
"throwOnValidationError", true, // default
)
));
import "context"
client.Databases.Insert(ctx, map[string]any{
"table": "users",
data: userData,
preSave: [...],
preSaveConfig: {
encryptionKey: os.Getenv("ENCRYPTION_KEY"),
"defaultHashAlgorithm": "bcrypt",
"defaultSaltRounds": 12,
"throwOnValidationError": true, // default
},
});
await ductape.Database.Insert(new Dictionary<string, object?>
{
["table"] = "users",
data: userData,
preSave: [...],
preSaveConfig: {
encryptionKey: Environment.GetEnvironmentVariable("ENCRYPTION_KEY"),
["defaultHashAlgorithm"] = "bcrypt",
["defaultSaltRounds"] = 12,
["throwOnValidationError"] = true, // default
},
});
Conditional Operations
Skip operations based on conditions:
- TypeScript
- Java
- Go
- .NET
{
field: 'password',
type: PreSaveOperationType.HASH,
algorithm: 'bcrypt',
// Only hash if password is being updated
condition: (record) => record.password !== undefined,
skipIfNull: true,
skipIfEmpty: true,
}
Map.of(
"field", "password",
type: PreSaveOperationType.HASH,
"algorithm", "bcrypt",
// Only hash if password is being updated
condition: (record) => record.password !== undefined,
"skipIfNull", true,
"skipIfEmpty", true
)
{
"field": "password",
type: PreSaveOperationType.HASH,
"algorithm": "bcrypt",
// Only hash if password is being updated
condition: (record) => record.password !== undefined,
"skipIfNull": true,
"skipIfEmpty": true,
}
{
["field"] = "password",
type: PreSaveOperationType.HASH,
["algorithm"] = "bcrypt",
// Only hash if password is being updated
condition: (record) => record.password !== undefined,
["skipIfNull"] = true,
["skipIfEmpty"] = true,
}
Complete Example
Here's a complete example combining multiple pre-save operations:
- TypeScript
- Java
- Go
- .NET
import { PreSave, PreSaveOperationType } from '@ductape/sdk';
// Create a user with comprehensive pre-save processing
const result = await ductape.databases.insert({
table: 'users',
data: {
first_name: ' John ',
last_name: ' Doe ',
email: 'John.Doe@EXAMPLE.COM',
password: 'secretpassword123',
phone: '(555) 123-4567',
bio: '<script>alert("xss")</script>I am a developer.',
website: 'https://example.com',
age: 25,
credit_card: '4111111111111111',
},
preSave: [
// Validation first
PreSave.validate('email', [
{ type: 'required' },
{ type: 'email' },
]),
PreSave.validate('password', [
{ type: 'minLength', value: 8, message: 'Password must be at least 8 characters' },
]),
PreSave.validate('age', [
{ type: 'min', value: 18 },
]),
PreSave.validate('website', [
{ type: 'url' },
]),
// String normalization
PreSave.trim('first_name'),
PreSave.trim('last_name'),
PreSave.normalizeEmail('email'),
PreSave.normalizePhone('phone'),
PreSave.sanitize('bio'),
// Security
PreSave.hash('password', { algorithm: 'bcrypt', saltRounds: 12 }),
PreSave.mask('credit_card', 'credit-card', {
storeOriginalIn: 'credit_card_encrypted',
}),
PreSave.encrypt('credit_card_encrypted'),
// Generated fields
PreSave.uuid('public_id'),
PreSave.timestamp('created_at'),
PreSave.slug('username', 'email', { maxLength: 30 }),
// Computed fields
PreSave.compute('full_name', (record) =>
`${record.first_name} ${record.last_name}`
),
],
preSaveConfig: {
encryptionKey: process.env.ENCRYPTION_KEY,
},
returning: true,
});
console.log(result.data[0]);
// {
// first_name: 'John',
// last_name: 'Doe',
// full_name: 'John Doe',
// email: 'john.doe@example.com',
// password: '$pbkdf2$12$...', // hashed
// phone: '5551234567',
// bio: 'I am a developer.',
// website: 'https://example.com',
// age: 25,
// credit_card: '************1111', // masked
// credit_card_encrypted: 'abc123...', // encrypted original
// public_id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
// username: 'john-doe-example-com',
// created_at: '2024-01-15T10:30:00.000Z',
// }
import Map.of( PreSave, PreSaveOperationType ) from '@ductape/sdk';
// Create a user with comprehensive pre-save processing
Map<String, Object> result = ductape.databases().insert(Map<String, Object>.of(
"table", "users",
data: Map.of(
"first_name", " John ",
"last_name", " Doe ",
"email", "John.Doe@EXAMPLE.COM",
"password", "secretpassword123",
"phone", "(555) 123-4567",
"bio", "<script>alert("xss")</script>I am a developer.",
"website", "https://example.com",
"age", 25,
"credit_card", "4111111111111111"
),
preSave: [
// Validation first
PreSave.validate('email', [
Map.of( "type", "required" ),
Map.of( "type", "email" ),
]),
PreSave.validate('password', [
Map.of( "type", "minLength", "value", 8, "message", "Password must be at least 8 characters" ),
]),
PreSave.validate('age', [
Map.of( "type", "min", "value", 18 ),
]),
PreSave.validate('website', [
Map.of( "type", "url" ),
]),
// String normalization
PreSave.trim('first_name'),
PreSave.trim('last_name'),
PreSave.normalizeEmail('email'),
PreSave.normalizePhone('phone'),
PreSave.sanitize('bio'),
// Security
PreSave.hash('password', Map.of( "algorithm", "bcrypt", "saltRounds", 12 )),
PreSave.mask('credit_card', 'credit-card', Map.of(
"storeOriginalIn", "credit_card_encrypted"
)),
PreSave.encrypt('credit_card_encrypted'),
// Generated fields
PreSave.uuid('public_id'),
PreSave.timestamp('created_at'),
PreSave.slug('username', 'email', Map.of( "maxLength", 30 )),
// Computed fields
PreSave.compute('full_name', (record) =>
`$Map.of(record.first_name) $Map.of(record.last_name)`
),
],
preSaveConfig: Map.of(
encryptionKey: System.getenv("ENCRYPTION_KEY")
),
"returning", true
));
System.out.println(result.data[0]);
// Map.of(
// "first_name", "John",
// "last_name", "Doe",
// "full_name", "John Doe",
// "email", "john.doe@example.com",
// "password", "$pbkdf2$12$...", // hashed
// "phone", "5551234567",
// "bio", "I am a developer.",
// "website", "https://example.com",
// "age", 25,
// "credit_card", "************1111", // masked
// "credit_card_encrypted", "abc123...", // encrypted original
// "public_id", "f47ac10b-58cc-4372-a567-0e02b2c3d479",
// "username", "john-doe-example-com",
// "created_at", "2024-01-"15T10", 30:00.000Z",
// )
import "context"
import { PreSave, PreSaveOperationType } from '@ductape/sdk';
// Create a user with comprehensive pre-save processing
result := client.Databases.Insert(ctx, map[string]any{
"table": "users",
data: {
"first_name": " John ",
"last_name": " Doe ",
"email": "John.Doe@EXAMPLE.COM",
"password": "secretpassword123",
"phone": "(555) 123-4567",
"bio": "<script>alert("xss")</script>I am a developer.",
"website": "https://example.com",
"age": 25,
"credit_card": "4111111111111111",
},
preSave: [
// Validation first
PreSave.validate('email', [
{ "type": "required" },
{ "type": "email" },
]),
PreSave.validate('password', [
{ "type": "minLength", "value": 8, "message": "Password must be at least 8 characters" },
]),
PreSave.validate('age', [
{ "type": "min", "value": 18 },
]),
PreSave.validate('website', [
{ "type": "url" },
]),
// String normalization
PreSave.trim('first_name'),
PreSave.trim('last_name'),
PreSave.normalizeEmail('email'),
PreSave.normalizePhone('phone'),
PreSave.sanitize('bio'),
// Security
PreSave.hash('password', { "algorithm": "bcrypt", "saltRounds": 12 }),
PreSave.mask('credit_card', 'credit-card', {
"storeOriginalIn": "credit_card_encrypted",
}),
PreSave.encrypt('credit_card_encrypted'),
// Generated fields
PreSave.uuid('public_id'),
PreSave.timestamp('created_at'),
PreSave.slug('username', 'email', { "maxLength": 30 }),
// Computed fields
PreSave.compute('full_name', (record) =>
`${record.first_name} ${record.last_name}`
),
],
preSaveConfig: {
encryptionKey: os.Getenv("ENCRYPTION_KEY"),
},
"returning": true,
});
fmt.Println(result.data[0]);
// {
// "first_name": "John",
// "last_name": "Doe",
// "full_name": "John Doe",
// "email": "john.doe@example.com",
// "password": "$pbkdf2$12$...", // hashed
// "phone": "5551234567",
// "bio": "I am a developer.",
// "website": "https://example.com",
// "age": 25,
// "credit_card": "************1111", // masked
// "credit_card_encrypted": "abc123...", // encrypted original
// "public_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
// "username": "john-doe-example-com",
// "created_at": "2024-01-"15T10": 30:00.000Z",
// }
import { PreSave, PreSaveOperationType } from '@ductape/sdk';
// Create a user with comprehensive pre-save processing
var result = await ductape.Database.Insert(new Dictionary<string, object?>
{
["table"] = "users",
data: {
["first_name"] = " John ",
["last_name"] = " Doe ",
["email"] = "John.Doe@EXAMPLE.COM",
["password"] = "secretpassword123",
["phone"] = "(555) 123-4567",
["bio"] = "<script>alert("xss")</script>I am a developer.",
["website"] = "https://example.com",
["age"] = 25,
["credit_card"] = "4111111111111111",
},
preSave: [
// Validation first
PreSave.validate('email', [
{ ["type"] = "required" },
{ ["type"] = "email" },
]),
PreSave.validate('password', [
{ ["type"] = "minLength", ["value"] = 8, ["message"] = "Password must be at least 8 characters" },
]),
PreSave.validate('age', [
{ ["type"] = "min", ["value"] = 18 },
]),
PreSave.validate('website', [
{ ["type"] = "url" },
]),
// String normalization
PreSave.trim('first_name'),
PreSave.trim('last_name'),
PreSave.normalizeEmail('email'),
PreSave.normalizePhone('phone'),
PreSave.sanitize('bio'),
// Security
PreSave.hash('password', { ["algorithm"] = "bcrypt", ["saltRounds"] = 12 }),
PreSave.mask('credit_card', 'credit-card', {
["storeOriginalIn"] = "credit_card_encrypted",
}),
PreSave.encrypt('credit_card_encrypted'),
// Generated fields
PreSave.uuid('public_id'),
PreSave.timestamp('created_at'),
PreSave.slug('username', 'email', { ["maxLength"] = 30 }),
// Computed fields
PreSave.compute('full_name', (record) =>
`${record.first_name} ${record.last_name}`
),
],
preSaveConfig: {
encryptionKey: Environment.GetEnvironmentVariable("ENCRYPTION_KEY"),
},
["returning"] = true,
});
Console.WriteLine(result.data[0]);
// {
// ["first_name"] = "John",
// ["last_name"] = "Doe",
// ["full_name"] = "John Doe",
// ["email"] = "john.doe@example.com",
// ["password"] = "$pbkdf2$12$...", // hashed
// ["phone"] = "5551234567",
// ["bio"] = "I am a developer.",
// ["website"] = "https://example.com",
// ["age"] = 25,
// ["credit_card"] = "************1111", // masked
// ["credit_card_encrypted"] = "abc123...", // encrypted original
// ["public_id"] = "f47ac10b-58cc-4372-a567-0e02b2c3d479",
// ["username"] = "john-doe-example-com",
// ["created_at"] = "2024-01-["15T10"] = 30:00.000Z",
// }
Operation Execution Order
Pre-save operations are automatically sorted and executed in this order:
- Default values - Apply defaults first
- Computed fields - Calculate derived values
- Validation - Validate before transformations
- String operations - trim, lowercase, uppercase, normalize
- Sanitization - Clean HTML/scripts
- Truncation - Limit string lengths
- Slug generation - Generate slugs
- Number operations - round, clamp
- JSON parsing - Parse/stringify JSON
- UUID/Timestamp - Generate identifiers
- Custom transforms - Apply transformations
- Masking - Mask sensitive data
- Hashing - Hash passwords (one-way)
- Encryption - Encrypt data (last, after all transforms)
Best Practices
1. Always Hash Passwords
Never store plain-text passwords. Use bcrypt with appropriate salt rounds:
- TypeScript
- Java
- Go
- .NET
PreSave.hash('password', { algorithm: 'bcrypt', saltRounds: 12 })
PreSave.hash('password', Map.of( "algorithm", "bcrypt", "saltRounds", 12 ))
PreSave.hash('password', { "algorithm": "bcrypt", "saltRounds": 12 })
PreSave.hash('password', { ["algorithm"] = "bcrypt", ["saltRounds"] = 12 })
2. Encrypt Sensitive Data
Encrypt API keys, tokens, and PII that needs to be retrieved later:
- TypeScript
- Java
- Go
- .NET
PreSave.encrypt('api_key')
PreSave.encrypt('ssn')
PreSave.encrypt('api_key')
PreSave.encrypt('ssn')
PreSave.encrypt('api_key')
PreSave.encrypt('ssn')
PreSave.encrypt('api_key')
PreSave.encrypt('ssn')
3. Validate at the Database Layer
Don't rely solely on frontend validation:
- TypeScript
- Java
- Go
- .NET
PreSave.validate('email', [
{ type: 'required' },
{ type: 'email' },
{ type: 'maxLength', value: 255 },
])
PreSave.validate('email', [
Map.of( "type", "required" ),
Map.of( "type", "email" ),
Map.of( "type", "maxLength", "value", 255 ),
])
PreSave.validate('email', [
{ "type": "required" },
{ "type": "email" },
{ "type": "maxLength", "value": 255 },
])
PreSave.validate('email', [
{ ["type"] = "required" },
{ ["type"] = "email" },
{ ["type"] = "maxLength", ["value"] = 255 },
])
4. Sanitize User Input
Always sanitize user-generated content to prevent XSS:
- TypeScript
- Java
- Go
- .NET
PreSave.sanitize('comment')
PreSave.sanitize('bio')
PreSave.sanitize('comment')
PreSave.sanitize('bio')
PreSave.sanitize('comment')
PreSave.sanitize('bio')
PreSave.sanitize('comment')
PreSave.sanitize('bio')
5. Normalize Before Unique Checks
Normalize emails and other unique fields to prevent duplicates:
- TypeScript
- Java
- Go
- .NET
PreSave.normalizeEmail('email') // Before unique constraint check
PreSave.normalizeEmail('email') // Before unique constraint check
PreSave.normalizeEmail('email') // Before unique constraint check
PreSave.normalizeEmail('email') // Before unique constraint check
6. Use Masking for Display Data
When you need to show partial sensitive data:
- TypeScript
- Java
- Go
- .NET
PreSave.mask('card_number', 'credit-card', {
storeOriginalIn: 'card_number_full', // Store encrypted original
})
PreSave.mask('card_number', 'credit-card', Map.of(
"storeOriginalIn", "card_number_full", // Store encrypted original
))
PreSave.mask('card_number', 'credit-card', {
"storeOriginalIn": "card_number_full", // Store encrypted original
})
PreSave.mask('card_number', 'credit-card', {
["storeOriginalIn"] = "card_number_full", // Store encrypted original
})
TypeScript Support
All pre-save operations are fully typed:
- TypeScript
- Java
- Go
- .NET
import {
PreSave,
PreSaveOperationType,
IPreSaveOperation,
IValidationRule,
} from '@ductape/sdk';
// Type-safe operation creation
const operations: IPreSaveOperation[] = [
PreSave.hash('password'),
PreSave.validate('email', [
{ type: 'email' } as IValidationRule,
]),
];
import Map.of(
PreSave,
PreSaveOperationType,
IPreSaveOperation,
IValidationRule
) from '@ductape/sdk';
// Type-safe operation creation
Map<String, Object> operations: IPreSaveOperation[] = [
PreSave.hash('password'),
PreSave.validate('email', [
Map.of( "type", "email" ) as IValidationRule,
]),
];
import {
PreSave,
PreSaveOperationType,
IPreSaveOperation,
IValidationRule,
} from '@ductape/sdk';
// Type-safe operation creation
const operations: IPreSaveOperation[] = [
PreSave.hash('password'),
PreSave.validate('email', [
{ "type": "email" } as IValidationRule,
]),
];
import {
PreSave,
PreSaveOperationType,
IPreSaveOperation,
IValidationRule,
} from '@ductape/sdk';
// Type-safe operation creation
var operations: IPreSaveOperation[] = [
PreSave.hash('password'),
PreSave.validate('email', [
{ ["type"] = "email" } as IValidationRule,
]),
];
See Also
- Writing Data - Basic insert and update operations
- Table Management - Schema definition with field constraints
- Best Practices - Security and performance guidelines