@kardoe/quickback 0.5.11 → 0.5.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +45 -0
- package/dist/commands/compile.js.map +1 -1
- package/dist/docs/content.d.ts.map +1 -1
- package/dist/docs/content.js +61 -46
- package/dist/docs/content.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/file-loader.d.ts.map +1 -1
- package/dist/lib/file-loader.js +42 -17
- package/dist/lib/file-loader.js.map +1 -1
- package/dist/lib/file-loader.test.d.ts +2 -0
- package/dist/lib/file-loader.test.d.ts.map +1 -0
- package/dist/lib/file-loader.test.js +65 -0
- package/dist/lib/file-loader.test.js.map +1 -0
- package/package.json +1 -1
- package/src/skill/SKILL.md +142 -95
- package/src/skill/agents/quickback-specialist/AGENT.md +44 -38
package/src/skill/SKILL.md
CHANGED
|
@@ -100,42 +100,40 @@ Features live in `quickback/features/{name}/` with one file per table using `def
|
|
|
100
100
|
|
|
101
101
|
**Important**: Legacy mode (separate schema.ts + resource.ts) is no longer supported.
|
|
102
102
|
|
|
103
|
-
## Example:
|
|
103
|
+
## Example: candidates.ts
|
|
104
104
|
|
|
105
105
|
```typescript
|
|
106
|
-
import { sqliteTable, text
|
|
106
|
+
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
107
107
|
import { defineTable } from "@quickback/compiler";
|
|
108
108
|
|
|
109
|
-
export const
|
|
110
|
-
id:
|
|
111
|
-
title: text("title").notNull(),
|
|
112
|
-
description: text("description"),
|
|
113
|
-
completed: integer("completed", { mode: "boolean" }).default(false),
|
|
114
|
-
userId: text("user_id").notNull(),
|
|
109
|
+
export const candidates = sqliteTable("candidates", {
|
|
110
|
+
id: text("id").primaryKey(),
|
|
115
111
|
organizationId: text("organization_id").notNull(),
|
|
112
|
+
name: text("name").notNull(),
|
|
113
|
+
email: text("email").notNull(),
|
|
114
|
+
phone: text("phone"),
|
|
115
|
+
resumeUrl: text("resume_url"),
|
|
116
|
+
source: text("source"),
|
|
116
117
|
});
|
|
117
118
|
|
|
118
|
-
export default defineTable(
|
|
119
|
+
export default defineTable(candidates, {
|
|
119
120
|
firewall: {
|
|
120
|
-
organization: {},
|
|
121
|
-
owner: {}, // Auto-detects 'userId' column
|
|
121
|
+
organization: {},
|
|
122
122
|
},
|
|
123
|
-
|
|
124
123
|
crud: {
|
|
125
|
-
list: { access: { roles: ["
|
|
126
|
-
get: { access: { roles: ["
|
|
127
|
-
create: { access: { roles: ["
|
|
128
|
-
update: { access: { roles: ["
|
|
129
|
-
delete: { access: { roles: ["
|
|
124
|
+
list: { access: { roles: ["owner", "hiring-manager", "recruiter", "interviewer"] } },
|
|
125
|
+
get: { access: { roles: ["owner", "hiring-manager", "recruiter", "interviewer"] } },
|
|
126
|
+
create: { access: { roles: ["owner", "hiring-manager", "recruiter"] } },
|
|
127
|
+
update: { access: { roles: ["owner", "hiring-manager", "recruiter"] } },
|
|
128
|
+
delete: { access: { roles: ["owner", "hiring-manager"] }, mode: "soft" },
|
|
130
129
|
},
|
|
131
|
-
|
|
132
130
|
guards: {
|
|
133
|
-
createable: ["
|
|
134
|
-
updatable: ["
|
|
131
|
+
createable: ["name", "email", "phone", "resumeUrl", "source"],
|
|
132
|
+
updatable: ["name", "phone"],
|
|
135
133
|
},
|
|
136
|
-
|
|
137
134
|
masking: {
|
|
138
|
-
|
|
135
|
+
email: { type: "email", show: { roles: ["hiring-manager", "recruiter"] } },
|
|
136
|
+
phone: { type: "phone", show: { roles: ["hiring-manager", "recruiter"] } },
|
|
139
137
|
},
|
|
140
138
|
});
|
|
141
139
|
```
|
|
@@ -146,9 +144,12 @@ Tables without a `defineTable()` default export get no API routes:
|
|
|
146
144
|
|
|
147
145
|
```typescript
|
|
148
146
|
// No default export = no API routes generated
|
|
149
|
-
export const
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
export const interviewScores = sqliteTable('interview_scores', {
|
|
148
|
+
applicationId: text('application_id').notNull(),
|
|
149
|
+
interviewerId: text('interviewer_id').notNull(),
|
|
150
|
+
score: integer('score').notNull(),
|
|
151
|
+
feedback: text('feedback'),
|
|
152
|
+
organizationId: text('organization_id').notNull(), // Always scope junction tables
|
|
152
153
|
});
|
|
153
154
|
```
|
|
154
155
|
|
|
@@ -201,18 +202,18 @@ Role-based and record-based access control. Deny by default.
|
|
|
201
202
|
|
|
202
203
|
```typescript
|
|
203
204
|
crud: {
|
|
204
|
-
list: { access: { roles: ['
|
|
205
|
-
get: { access: { roles: ['
|
|
206
|
-
create: { access: { roles: ['
|
|
205
|
+
list: { access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] } },
|
|
206
|
+
get: { access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] } },
|
|
207
|
+
create: { access: { roles: ['owner', 'hiring-manager', 'recruiter'] } },
|
|
207
208
|
update: {
|
|
208
209
|
access: {
|
|
209
210
|
or: [
|
|
210
|
-
{ roles: ['
|
|
211
|
+
{ roles: ['owner', 'hiring-manager'] },
|
|
211
212
|
{ record: { createdBy: { equals: '$ctx.userId' } } },
|
|
212
213
|
],
|
|
213
214
|
},
|
|
214
215
|
},
|
|
215
|
-
delete: { access: { roles: ['
|
|
216
|
+
delete: { access: { roles: ['owner', 'hiring-manager'] } },
|
|
216
217
|
}
|
|
217
218
|
```
|
|
218
219
|
|
|
@@ -243,10 +244,10 @@ Controls which fields can be modified.
|
|
|
243
244
|
|
|
244
245
|
```typescript
|
|
245
246
|
guards: {
|
|
246
|
-
createable: ['
|
|
247
|
-
updatable: ['
|
|
248
|
-
immutable: ['
|
|
249
|
-
protected: {
|
|
247
|
+
createable: ['candidateId', 'jobId', 'notes'],
|
|
248
|
+
updatable: ['notes'],
|
|
249
|
+
immutable: ['appliedAt'],
|
|
250
|
+
protected: { stage: ['advance-stage'] },
|
|
250
251
|
}
|
|
251
252
|
```
|
|
252
253
|
|
|
@@ -281,13 +282,13 @@ export default defineTable(external_orders, {
|
|
|
281
282
|
|
|
282
283
|
```typescript
|
|
283
284
|
masking: {
|
|
284
|
-
ssn: {
|
|
285
|
-
type: 'ssn', // *****6789
|
|
286
|
-
show: { roles: ['admin', 'hr'] },
|
|
287
|
-
},
|
|
288
285
|
email: {
|
|
289
|
-
type: 'email',
|
|
290
|
-
show: { roles: ['
|
|
286
|
+
type: 'email',
|
|
287
|
+
show: { roles: ['hiring-manager', 'recruiter'] },
|
|
288
|
+
},
|
|
289
|
+
phone: {
|
|
290
|
+
type: 'phone',
|
|
291
|
+
show: { roles: ['hiring-manager', 'recruiter'] },
|
|
291
292
|
},
|
|
292
293
|
}
|
|
293
294
|
```
|
|
@@ -323,45 +324,89 @@ The compiler warns if sensitive columns aren't explicitly configured:
|
|
|
323
324
|
Views are named projections that control which fields are visible based on role.
|
|
324
325
|
|
|
325
326
|
```typescript
|
|
326
|
-
export default defineTable(
|
|
327
|
+
export default defineTable(candidates, {
|
|
327
328
|
// ... firewall, guards, etc.
|
|
328
329
|
|
|
329
330
|
views: {
|
|
330
|
-
|
|
331
|
-
fields: ['id', 'name', 'email'],
|
|
332
|
-
access: { roles: ['
|
|
331
|
+
pipeline: {
|
|
332
|
+
fields: ['id', 'name', 'email', 'source'],
|
|
333
|
+
access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },
|
|
333
334
|
},
|
|
334
335
|
full: {
|
|
335
|
-
fields: ['id', 'name', 'email', 'phone', '
|
|
336
|
-
access: { roles: ['
|
|
336
|
+
fields: ['id', 'name', 'email', 'phone', 'resumeUrl', 'source'],
|
|
337
|
+
access: { roles: ['owner', 'hiring-manager', 'recruiter'] },
|
|
337
338
|
},
|
|
338
339
|
},
|
|
339
340
|
});
|
|
340
341
|
```
|
|
341
342
|
|
|
342
343
|
**Generated endpoints**:
|
|
343
|
-
- `GET /api/v1/
|
|
344
|
-
- `GET /api/v1/
|
|
344
|
+
- `GET /api/v1/candidates/views/pipeline` — returns only id, name, email, source
|
|
345
|
+
- `GET /api/v1/candidates/views/full` — returns all fields (recruiters and above)
|
|
345
346
|
|
|
346
347
|
Views support the same query parameters as list (filtering, sorting, pagination). Masking still applies to returned fields.
|
|
347
348
|
|
|
348
349
|
---
|
|
349
350
|
|
|
351
|
+
# References — FK Target Mapping
|
|
352
|
+
|
|
353
|
+
When FK columns don't match the target table name by convention (e.g., `vendorId` points to the `contact` table), declare explicit references:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
export default defineTable(items, {
|
|
357
|
+
references: {
|
|
358
|
+
clientId: "contact",
|
|
359
|
+
vendorId: "contact",
|
|
360
|
+
salesCodeId: "salesCode",
|
|
361
|
+
taxLocationId: "taxLocation",
|
|
362
|
+
},
|
|
363
|
+
// ...
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Each key is a column name ending in `Id`, value is the camelCase target table name. These flow into schema-registry.json as `fkTarget` on each column, enabling the CMS to render typeahead/lookup inputs.
|
|
368
|
+
|
|
369
|
+
Convention-based matching (strip `Id` suffix) still works for simple cases like `projectId` → `project`. Use `references` only when the convention doesn't match.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
# Input Hints — CMS Form Controls
|
|
374
|
+
|
|
375
|
+
Control how the CMS renders form inputs for specific columns:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
export default defineTable(invoice, {
|
|
379
|
+
inputHints: {
|
|
380
|
+
status: "select",
|
|
381
|
+
sortOrder: "radio",
|
|
382
|
+
isPartialPaymentDisabled: "checkbox",
|
|
383
|
+
headerMessage: "textarea",
|
|
384
|
+
footerMessage: "textarea",
|
|
385
|
+
},
|
|
386
|
+
// ...
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Available values: `select`, `multi-select`, `radio`, `checkbox`, `textarea`, `lookup`, `hidden`, `color`, `date`, `datetime`, `time`, `currency`.
|
|
391
|
+
|
|
392
|
+
Input hints are emitted in schema-registry.json as `inputHints` on the table metadata.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
350
396
|
# Validation
|
|
351
397
|
|
|
352
398
|
Field-level validation rules compiled into the API:
|
|
353
399
|
|
|
354
400
|
```typescript
|
|
355
|
-
export default defineTable(
|
|
401
|
+
export default defineTable(jobs, {
|
|
356
402
|
// ... other config
|
|
357
403
|
|
|
358
404
|
validation: {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
code: { pattern: '^[A-Z]{3}$' },
|
|
405
|
+
title: { minLength: 1, maxLength: 200 },
|
|
406
|
+
department: { minLength: 1, maxLength: 100 },
|
|
407
|
+
status: { enum: ['draft', 'open', 'closed'] },
|
|
408
|
+
salaryMin: { min: 0 },
|
|
409
|
+
salaryMax: { min: 0 },
|
|
365
410
|
},
|
|
366
411
|
});
|
|
367
412
|
```
|
|
@@ -373,51 +418,54 @@ export default defineTable(rooms, {
|
|
|
373
418
|
Actions are custom API endpoints for business logic beyond CRUD. Defined in a separate `actions.ts` file using `defineActions()`.
|
|
374
419
|
|
|
375
420
|
```typescript
|
|
376
|
-
// quickback/features/
|
|
377
|
-
import {
|
|
421
|
+
// quickback/features/applications/actions.ts
|
|
422
|
+
import { applications } from './applications';
|
|
378
423
|
import { defineActions } from '@quickback/compiler';
|
|
379
424
|
import { z } from 'zod';
|
|
380
425
|
|
|
381
|
-
export default defineActions(
|
|
382
|
-
|
|
383
|
-
description: "
|
|
426
|
+
export default defineActions(applications, {
|
|
427
|
+
'advance-stage': {
|
|
428
|
+
description: "Move application to the next pipeline stage",
|
|
384
429
|
input: z.object({
|
|
385
|
-
|
|
430
|
+
stage: z.enum(["screening", "interview", "offer", "hired"]),
|
|
431
|
+
notes: z.string().optional(),
|
|
386
432
|
}),
|
|
387
|
-
|
|
388
|
-
roles: ["
|
|
389
|
-
record: {
|
|
433
|
+
access: {
|
|
434
|
+
roles: ["owner", "hiring-manager"],
|
|
435
|
+
record: { stage: { notEquals: "rejected" } },
|
|
390
436
|
},
|
|
391
437
|
execute: async ({ db, record, ctx, input }) => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
438
|
+
const [updated] = await db.update(applications)
|
|
439
|
+
.set({ stage: input.stage, notes: input.notes ?? record.notes })
|
|
440
|
+
.where(eq(applications.id, record.id))
|
|
441
|
+
.returning();
|
|
442
|
+
return updated;
|
|
395
443
|
},
|
|
396
444
|
sideEffects: "sync",
|
|
397
445
|
},
|
|
398
446
|
|
|
399
|
-
|
|
400
|
-
description: "
|
|
401
|
-
input: z.object({}),
|
|
402
|
-
|
|
403
|
-
handler: "./handlers/
|
|
447
|
+
reject: {
|
|
448
|
+
description: "Reject an application",
|
|
449
|
+
input: z.object({ reason: z.string() }),
|
|
450
|
+
access: { roles: ["owner", "hiring-manager"] },
|
|
451
|
+
handler: "./handlers/reject", // OR: external file handler
|
|
404
452
|
},
|
|
405
453
|
});
|
|
406
454
|
```
|
|
407
455
|
|
|
408
|
-
**Generated Route**: `POST /api/v1/
|
|
456
|
+
**Generated Route**: `POST /api/v1/applications/:id/advance-stage`
|
|
409
457
|
|
|
410
458
|
### Record-based vs Standalone Actions
|
|
411
459
|
|
|
412
460
|
**Record-based** (default) — operates on a specific record via `:id`:
|
|
413
461
|
```typescript
|
|
414
|
-
|
|
415
|
-
description: "
|
|
416
|
-
input: z.object({
|
|
417
|
-
|
|
418
|
-
handler: "./handlers/
|
|
462
|
+
'advance-stage': {
|
|
463
|
+
description: "Move application to the next pipeline stage",
|
|
464
|
+
input: z.object({ stage: z.enum(["screening", "interview", "offer", "hired"]) }),
|
|
465
|
+
access: { roles: ["owner", "hiring-manager"] },
|
|
466
|
+
handler: "./handlers/advance-stage",
|
|
419
467
|
}
|
|
420
|
-
// → POST /api/v1/
|
|
468
|
+
// → POST /api/v1/applications/:id/advance-stage
|
|
421
469
|
```
|
|
422
470
|
|
|
423
471
|
**Standalone** — custom endpoint not tied to a record:
|
|
@@ -428,10 +476,10 @@ chat: {
|
|
|
428
476
|
method: "POST",
|
|
429
477
|
responseType: "stream",
|
|
430
478
|
input: z.object({ message: z.string() }),
|
|
431
|
-
|
|
479
|
+
access: { roles: ["recruiter", "hiring-manager"] },
|
|
432
480
|
handler: "./handlers/chat",
|
|
433
481
|
}
|
|
434
|
-
// → POST /api/v1/
|
|
482
|
+
// → POST /api/v1/applications/chat
|
|
435
483
|
```
|
|
436
484
|
|
|
437
485
|
### Action Options
|
|
@@ -535,7 +583,7 @@ OR'd LIKE across all `text()` schema columns (system columns excluded). Combine
|
|
|
535
583
|
|
|
536
584
|
```json
|
|
537
585
|
{
|
|
538
|
-
"data": [{ "id": "...", "
|
|
586
|
+
"data": [{ "id": "...", "name": "Jane Smith", "source": "linkedin" }],
|
|
539
587
|
"pagination": {
|
|
540
588
|
"limit": 10,
|
|
541
589
|
"offset": 0,
|
|
@@ -718,16 +766,15 @@ Detect from `quickback.config.ts` and use the correct imports:
|
|
|
718
766
|
|
|
719
767
|
Full documentation at https://docs.quickback.dev
|
|
720
768
|
|
|
721
|
-
- [
|
|
722
|
-
- [Concepts](https://docs.quickback.dev/definitions/concepts)
|
|
723
|
-
- [
|
|
724
|
-
- [
|
|
725
|
-
- [
|
|
726
|
-
- [
|
|
727
|
-
- [
|
|
728
|
-
- [
|
|
729
|
-
- [
|
|
730
|
-
- [
|
|
731
|
-
- [
|
|
732
|
-
- [
|
|
733
|
-
- [Quick Reference](https://docs.quickback.dev/stack/reference)
|
|
769
|
+
- [Getting Started](https://docs.quickback.dev/compiler/getting-started)
|
|
770
|
+
- [Concepts](https://docs.quickback.dev/compiler/definitions/concepts)
|
|
771
|
+
- [Full Example](https://docs.quickback.dev/compiler/getting-started/full-example)
|
|
772
|
+
- [Database Schema](https://docs.quickback.dev/compiler/definitions/schema)
|
|
773
|
+
- [CRUD & API](https://docs.quickback.dev/compiler/using-the-api)
|
|
774
|
+
- [Views](https://docs.quickback.dev/compiler/definitions/views)
|
|
775
|
+
- [Actions](https://docs.quickback.dev/compiler/definitions/actions)
|
|
776
|
+
- [Firewall](https://docs.quickback.dev/compiler/definitions/firewall)
|
|
777
|
+
- [Access](https://docs.quickback.dev/compiler/definitions/access)
|
|
778
|
+
- [Guards](https://docs.quickback.dev/compiler/definitions/guards)
|
|
779
|
+
- [Masking](https://docs.quickback.dev/compiler/definitions/masking)
|
|
780
|
+
- [CLI Reference](https://docs.quickback.dev/compiler/cloud-compiler/cli)
|
|
@@ -51,35 +51,37 @@ Detect the database dialect from `quickback.config.ts`:
|
|
|
51
51
|
### Table Definition Pattern
|
|
52
52
|
|
|
53
53
|
```typescript
|
|
54
|
-
import { sqliteTable, text
|
|
54
|
+
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
55
55
|
import { defineTable } from "@quickback/compiler";
|
|
56
56
|
|
|
57
|
-
export const
|
|
58
|
-
id:
|
|
59
|
-
title: text("title").notNull(),
|
|
60
|
-
completed: integer("completed", { mode: "boolean" }).default(false),
|
|
61
|
-
userId: text("user_id").notNull(),
|
|
57
|
+
export const candidates = sqliteTable("candidates", {
|
|
58
|
+
id: text("id").primaryKey(),
|
|
62
59
|
organizationId: text("organization_id").notNull(),
|
|
60
|
+
name: text("name").notNull(),
|
|
61
|
+
email: text("email").notNull(),
|
|
62
|
+
phone: text("phone"),
|
|
63
|
+
resumeUrl: text("resume_url"),
|
|
64
|
+
source: text("source"),
|
|
63
65
|
});
|
|
64
66
|
|
|
65
|
-
export default defineTable(
|
|
67
|
+
export default defineTable(candidates, {
|
|
66
68
|
firewall: {
|
|
67
|
-
organization: {},
|
|
68
|
-
owner: {}, // Auto-detects 'userId' column
|
|
69
|
+
organization: {},
|
|
69
70
|
},
|
|
70
71
|
crud: {
|
|
71
|
-
list: { access: { roles: ["
|
|
72
|
-
get: { access: { roles: ["
|
|
73
|
-
create: { access: { roles: ["
|
|
74
|
-
update: { access: { roles: ["
|
|
75
|
-
delete: { access: { roles: ["
|
|
72
|
+
list: { access: { roles: ["owner", "hiring-manager", "recruiter", "interviewer"] } },
|
|
73
|
+
get: { access: { roles: ["owner", "hiring-manager", "recruiter", "interviewer"] } },
|
|
74
|
+
create: { access: { roles: ["owner", "hiring-manager", "recruiter"] } },
|
|
75
|
+
update: { access: { roles: ["owner", "hiring-manager", "recruiter"] } },
|
|
76
|
+
delete: { access: { roles: ["owner", "hiring-manager"] }, mode: "soft" },
|
|
76
77
|
},
|
|
77
78
|
guards: {
|
|
78
|
-
createable: ["
|
|
79
|
-
updatable: ["
|
|
79
|
+
createable: ["name", "email", "phone", "resumeUrl", "source"],
|
|
80
|
+
updatable: ["name", "phone"],
|
|
80
81
|
},
|
|
81
82
|
masking: {
|
|
82
|
-
|
|
83
|
+
email: { type: "email", show: { roles: ["hiring-manager", "recruiter"] } },
|
|
84
|
+
phone: { type: "phone", show: { roles: ["hiring-manager", "recruiter"] } },
|
|
83
85
|
},
|
|
84
86
|
});
|
|
85
87
|
```
|
|
@@ -87,24 +89,28 @@ export default defineTable(todos, {
|
|
|
87
89
|
### Actions Pattern
|
|
88
90
|
|
|
89
91
|
```typescript
|
|
90
|
-
// quickback/features/
|
|
91
|
-
import {
|
|
92
|
+
// quickback/features/applications/actions.ts
|
|
93
|
+
import { applications } from './applications';
|
|
92
94
|
import { defineActions } from '@quickback/compiler';
|
|
93
95
|
import { z } from 'zod';
|
|
94
96
|
|
|
95
|
-
export default defineActions(
|
|
96
|
-
|
|
97
|
-
description: "
|
|
97
|
+
export default defineActions(applications, {
|
|
98
|
+
'advance-stage': {
|
|
99
|
+
description: "Move application to the next pipeline stage",
|
|
98
100
|
input: z.object({
|
|
99
|
-
|
|
101
|
+
stage: z.enum(["screening", "interview", "offer", "hired"]),
|
|
102
|
+
notes: z.string().optional(),
|
|
100
103
|
}),
|
|
101
|
-
|
|
102
|
-
roles: ["
|
|
103
|
-
record: {
|
|
104
|
+
access: {
|
|
105
|
+
roles: ["owner", "hiring-manager"],
|
|
106
|
+
record: { stage: { notEquals: "rejected" } },
|
|
104
107
|
},
|
|
105
108
|
execute: async ({ db, record, ctx, input }) => {
|
|
106
|
-
await db.update(
|
|
107
|
-
|
|
109
|
+
const [updated] = await db.update(applications)
|
|
110
|
+
.set({ stage: input.stage, notes: input.notes ?? record.notes })
|
|
111
|
+
.where(eq(applications.id, record.id))
|
|
112
|
+
.returning();
|
|
113
|
+
return updated;
|
|
108
114
|
},
|
|
109
115
|
},
|
|
110
116
|
});
|
|
@@ -137,29 +143,29 @@ firewall: {
|
|
|
137
143
|
### Workflow with protected status
|
|
138
144
|
```typescript
|
|
139
145
|
guards: {
|
|
140
|
-
protected: {
|
|
146
|
+
protected: { stage: ['advance-stage', 'reject'] }
|
|
141
147
|
}
|
|
142
|
-
// Plus defineActions() for
|
|
148
|
+
// Plus defineActions() for advance-stage/reject
|
|
143
149
|
```
|
|
144
150
|
|
|
145
151
|
### PII masking
|
|
146
152
|
```typescript
|
|
147
153
|
masking: {
|
|
148
|
-
email: { type: 'email', show: { roles: ['
|
|
149
|
-
|
|
154
|
+
email: { type: 'email', show: { roles: ['hiring-manager', 'recruiter'] } },
|
|
155
|
+
phone: { type: 'phone', show: { roles: ['hiring-manager', 'recruiter'] } },
|
|
150
156
|
}
|
|
151
157
|
```
|
|
152
158
|
|
|
153
159
|
### Views (column-level security)
|
|
154
160
|
```typescript
|
|
155
161
|
views: {
|
|
156
|
-
|
|
157
|
-
fields: ['id', 'name', '
|
|
158
|
-
access: { roles: ['
|
|
162
|
+
pipeline: {
|
|
163
|
+
fields: ['id', 'name', 'source', 'stage'],
|
|
164
|
+
access: { roles: ['owner', 'hiring-manager', 'recruiter', 'interviewer'] },
|
|
159
165
|
},
|
|
160
166
|
full: {
|
|
161
|
-
fields: ['id', 'name', 'email', 'phone', '
|
|
162
|
-
access: { roles: ['
|
|
167
|
+
fields: ['id', 'name', 'email', 'phone', 'resumeUrl', 'source', 'stage'],
|
|
168
|
+
access: { roles: ['owner', 'hiring-manager', 'recruiter'] },
|
|
163
169
|
},
|
|
164
170
|
}
|
|
165
171
|
```
|
|
@@ -173,7 +179,7 @@ chat: {
|
|
|
173
179
|
method: "POST",
|
|
174
180
|
responseType: "stream",
|
|
175
181
|
input: z.object({ message: z.string() }),
|
|
176
|
-
|
|
182
|
+
access: { roles: ["recruiter", "hiring-manager"] },
|
|
177
183
|
handler: "./handlers/chat",
|
|
178
184
|
}
|
|
179
185
|
```
|