cvx-kit 0.0.3 → 0.0.4
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/components/approvals/audit.mjs +1 -1
- package/package.json +2 -8
- package/src/test.ts +7 -5
- package/src/auth.ts +0 -473
- package/src/components/approvals/_generated/api.ts +0 -69
- package/src/components/approvals/_generated/component.ts +0 -335
- package/src/components/approvals/_generated/dataModel.ts +0 -60
- package/src/components/approvals/_generated/server.ts +0 -156
- package/src/components/approvals/audit.ts +0 -148
- package/src/components/approvals/client.ts +0 -321
- package/src/components/approvals/constants.ts +0 -51
- package/src/components/approvals/convex.config.ts +0 -10
- package/src/components/approvals/decisions.ts +0 -376
- package/src/components/approvals/functions.ts +0 -27
- package/src/components/approvals/requests.ts +0 -240
- package/src/components/approvals/schema.ts +0 -23
- package/src/components/approvals/validators.ts +0 -272
- package/src/components/approvals/workflow.ts +0 -204
- package/src/components/approvals/workflow_steps.ts +0 -189
- package/src/components/foundation/_generated/api.ts +0 -62
- package/src/components/foundation/_generated/component.ts +0 -29
- package/src/components/foundation/_generated/dataModel.ts +0 -60
- package/src/components/foundation/_generated/server.ts +0 -156
- package/src/components/foundation/client.ts +0 -301
- package/src/components/foundation/convex.config.ts +0 -3
- package/src/components/foundation/functions.ts +0 -10
- package/src/components/foundation/modules/command/__tests__/command.test.ts +0 -106
- package/src/components/foundation/modules/command/command.ts +0 -154
- package/src/components/foundation/modules/observability/__tests__/observability.test.ts +0 -79
- package/src/components/foundation/modules/observability/observability.ts +0 -95
- package/src/components/foundation/modules/query/__tests__/query.test.ts +0 -25
- package/src/components/foundation/modules/query/query.ts +0 -47
- package/src/components/foundation/result.ts +0 -77
- package/src/components/foundation/schema.ts +0 -4
- package/src/components/foundation/telemetry.ts +0 -23
- package/src/errors.ts +0 -25
- package/src/index.ts +0 -56
- package/src/tenancy.ts +0 -110
- package/src/triggers.ts +0 -124
- package/src/zod-table.ts +0 -283
package/src/tenancy.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
wrapDatabaseReader,
|
|
3
|
-
wrapDatabaseWriter,
|
|
4
|
-
type RLSConfig,
|
|
5
|
-
type Rules,
|
|
6
|
-
} from 'convex-helpers/server/rowLevelSecurity'
|
|
7
|
-
import type {
|
|
8
|
-
GenericDataModel,
|
|
9
|
-
TableNamesInDataModel,
|
|
10
|
-
} from 'convex/server'
|
|
11
|
-
import { defaultErrors, type ErrorFactory } from './errors'
|
|
12
|
-
|
|
13
|
-
export { wrapDatabaseReader, wrapDatabaseWriter }
|
|
14
|
-
export type { RLSConfig, Rules }
|
|
15
|
-
|
|
16
|
-
/** The server-owned tenancy field stamped on every tenant table row. */
|
|
17
|
-
export const TENANT_FIELD = 'tenant' as const
|
|
18
|
-
|
|
19
|
-
type TenantRow = { [TENANT_FIELD]?: unknown }
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Deny-oriented row-level security rules generated mechanically from a table
|
|
23
|
-
* registry: read/insert/modify pass only when the row's tenant matches.
|
|
24
|
-
* Registering a table here is the entire onboarding — there is no second
|
|
25
|
-
* per-table step to forget.
|
|
26
|
-
*/
|
|
27
|
-
export function createTenantRules<DataModel extends GenericDataModel>(
|
|
28
|
-
tenant: string,
|
|
29
|
-
tables: readonly TableNamesInDataModel<DataModel>[],
|
|
30
|
-
): Rules<unknown, DataModel> {
|
|
31
|
-
const owned = async (_ctx: unknown, doc: TenantRow) =>
|
|
32
|
-
doc[TENANT_FIELD] === tenant
|
|
33
|
-
return Object.fromEntries(
|
|
34
|
-
tables.map((table) => [
|
|
35
|
-
table,
|
|
36
|
-
{ read: owned, insert: owned, modify: owned },
|
|
37
|
-
]),
|
|
38
|
-
) as unknown as Rules<unknown, DataModel>
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* AND-composes rule sets: a row passes an operation only when every rule set
|
|
43
|
-
* that defines a rule for it passes. Lets tenant isolation and role-level
|
|
44
|
-
* rules coexist on the same wrapped database.
|
|
45
|
-
*/
|
|
46
|
-
export function composeRules<Ctx, DataModel extends GenericDataModel>(
|
|
47
|
-
...sets: readonly Rules<Ctx, DataModel>[]
|
|
48
|
-
): Rules<Ctx, DataModel> {
|
|
49
|
-
const tables = new Set(sets.flatMap((set) => Object.keys(set)))
|
|
50
|
-
const composed: Record<string, Record<string, unknown>> = {}
|
|
51
|
-
for (const table of tables) {
|
|
52
|
-
const operations: Record<string, unknown> = {}
|
|
53
|
-
for (const operation of ['read', 'insert', 'modify'] as const) {
|
|
54
|
-
const rules = sets
|
|
55
|
-
.map((set) => (set as Record<string, Record<string, unknown>>)[table])
|
|
56
|
-
.map((tableRules) => tableRules?.[operation])
|
|
57
|
-
.filter((rule): rule is (ctx: Ctx, doc: never) => Promise<boolean> =>
|
|
58
|
-
typeof rule === 'function',
|
|
59
|
-
)
|
|
60
|
-
if (rules.length === 0) continue
|
|
61
|
-
operations[operation] = async (ctx: Ctx, doc: never) => {
|
|
62
|
-
for (const rule of rules) {
|
|
63
|
-
if (!(await rule(ctx, doc))) return false
|
|
64
|
-
}
|
|
65
|
-
return true
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
composed[table] = operations
|
|
69
|
-
}
|
|
70
|
-
return composed as Rules<Ctx, DataModel>
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Loads a client-referenced document and verifies tenant ownership without
|
|
75
|
-
* disclosing existence: a missing row and a foreign row fail identically
|
|
76
|
-
* with REFERENCE_NOT_FOUND. Use for every id argument a client supplies.
|
|
77
|
-
*/
|
|
78
|
-
export async function requireTenantReference<
|
|
79
|
-
Document extends { [TENANT_FIELD]?: unknown },
|
|
80
|
-
>(
|
|
81
|
-
tenant: string,
|
|
82
|
-
load: () => Promise<Document | null>,
|
|
83
|
-
errors: ErrorFactory = defaultErrors,
|
|
84
|
-
): Promise<Document> {
|
|
85
|
-
const document = await load()
|
|
86
|
-
if (!document || document[TENANT_FIELD] !== tenant) {
|
|
87
|
-
return errors.throw({
|
|
88
|
-
code: 'REFERENCE_NOT_FOUND',
|
|
89
|
-
message: 'The referenced row is unavailable',
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
return document
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Asserts a loaded document belongs to the tenant; use on internal paths. */
|
|
96
|
-
export function assertTenantOwned<
|
|
97
|
-
Document extends { [TENANT_FIELD]?: unknown },
|
|
98
|
-
>(
|
|
99
|
-
tenant: string,
|
|
100
|
-
document: Document,
|
|
101
|
-
errors: ErrorFactory = defaultErrors,
|
|
102
|
-
): Document {
|
|
103
|
-
if (document[TENANT_FIELD] !== tenant) {
|
|
104
|
-
return errors.throw({
|
|
105
|
-
code: 'CROSS_TENANT_REFERENCE',
|
|
106
|
-
message: 'The document belongs to another tenant',
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
return document
|
|
110
|
-
}
|
package/src/triggers.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Triggers,
|
|
3
|
-
type Change,
|
|
4
|
-
type Trigger,
|
|
5
|
-
} from 'convex-helpers/server/triggers'
|
|
6
|
-
import type {
|
|
7
|
-
GenericDataModel,
|
|
8
|
-
TableNamesInDataModel,
|
|
9
|
-
} from 'convex/server'
|
|
10
|
-
|
|
11
|
-
export { Triggers }
|
|
12
|
-
export type { Change, Trigger }
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* One application-wide mutation interceptor registry. Pass it to
|
|
16
|
-
* createAuthFunctions({ triggers }) so every authMutation/roleMutation/
|
|
17
|
-
* systemMutation runs writes through it — trigger enforcement becomes
|
|
18
|
-
* structural, not a convention.
|
|
19
|
-
*/
|
|
20
|
-
export function createTriggers<
|
|
21
|
-
DataModel extends GenericDataModel,
|
|
22
|
-
>(): Triggers<DataModel> {
|
|
23
|
-
return new Triggers<DataModel>()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Marks tables as append-only evidence tables: inserts pass, every update or
|
|
28
|
-
* delete throws. The ontology pattern for votes/history/audit-style tables.
|
|
29
|
-
*/
|
|
30
|
-
export function appendOnly<DataModel extends GenericDataModel>(
|
|
31
|
-
triggers: Triggers<DataModel>,
|
|
32
|
-
...tables: readonly TableNamesInDataModel<DataModel>[]
|
|
33
|
-
) {
|
|
34
|
-
for (const table of tables) {
|
|
35
|
-
triggers.register(table, async (_ctx, change) => {
|
|
36
|
-
if (change.operation !== 'insert') {
|
|
37
|
-
throw new Error(`${table} is append-only`)
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Forbids hard deletes on the given tables (soft-delete discipline). */
|
|
44
|
-
export function noDelete<DataModel extends GenericDataModel>(
|
|
45
|
-
triggers: Triggers<DataModel>,
|
|
46
|
-
...tables: readonly TableNamesInDataModel<DataModel>[]
|
|
47
|
-
) {
|
|
48
|
-
for (const table of tables) {
|
|
49
|
-
triggers.register(table, async (_ctx, change) => {
|
|
50
|
-
if (change.operation === 'delete') {
|
|
51
|
-
throw new Error(`${table} rows cannot be deleted`)
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Post-image tenant invariant for tenantTable rows. Row-level security
|
|
59
|
-
* authorizes a modification against the PRE-image, so ownership must also be
|
|
60
|
-
* enforced against the post-image: inserts must carry a tenant, and updates
|
|
61
|
-
* may never reassign it. Register for every tenant table (same registry that
|
|
62
|
-
* drives createTenantRules).
|
|
63
|
-
*/
|
|
64
|
-
export function tenantOwnership<DataModel extends GenericDataModel>(
|
|
65
|
-
triggers: Triggers<DataModel>,
|
|
66
|
-
...tables: readonly TableNamesInDataModel<DataModel>[]
|
|
67
|
-
) {
|
|
68
|
-
for (const table of tables) {
|
|
69
|
-
triggers.register(table, async (_ctx, change) => {
|
|
70
|
-
if (change.operation === 'insert') {
|
|
71
|
-
const document = change.newDoc as { tenant?: unknown }
|
|
72
|
-
if (
|
|
73
|
-
typeof document.tenant !== 'string' ||
|
|
74
|
-
document.tenant.length === 0
|
|
75
|
-
) {
|
|
76
|
-
throw new Error(`${table} rows require a tenant`)
|
|
77
|
-
}
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
if (change.operation === 'update') {
|
|
81
|
-
const previous = change.oldDoc as { tenant?: unknown }
|
|
82
|
-
const current = change.newDoc as { tenant?: unknown }
|
|
83
|
-
if (previous.tenant !== current.tenant) {
|
|
84
|
-
throw new Error(`${table} tenant ownership cannot be reassigned`)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
})
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Maintains zod-table's opinionated lifecycle timestamps: createdAt and
|
|
93
|
-
* updatedAt on insert, updatedAt on every update. archivedAt stays under
|
|
94
|
-
* application control. Register once per table; writes made through the
|
|
95
|
-
* auth constructors keep these fields correct automatically.
|
|
96
|
-
*/
|
|
97
|
-
export function timestamps<DataModel extends GenericDataModel>(
|
|
98
|
-
triggers: Triggers<DataModel>,
|
|
99
|
-
...tables: readonly TableNamesInDataModel<DataModel>[]
|
|
100
|
-
) {
|
|
101
|
-
for (const table of tables) {
|
|
102
|
-
triggers.register(table, async (ctx, change) => {
|
|
103
|
-
if (change.operation === 'delete') return
|
|
104
|
-
const now = Date.now()
|
|
105
|
-
if (change.operation === 'insert') {
|
|
106
|
-
const document = change.newDoc as { createdAt?: number }
|
|
107
|
-
if (document.createdAt === undefined) {
|
|
108
|
-
await ctx.innerDb.patch(change.id, {
|
|
109
|
-
createdAt: now,
|
|
110
|
-
updatedAt: now,
|
|
111
|
-
} as never)
|
|
112
|
-
}
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
const previous = change.oldDoc as { updatedAt?: number }
|
|
116
|
-
const current = change.newDoc as { updatedAt?: number }
|
|
117
|
-
// A write that already moved updatedAt (including our own patch)
|
|
118
|
-
// is left alone — this is the recursion guard.
|
|
119
|
-
if (current.updatedAt === previous.updatedAt) {
|
|
120
|
-
await ctx.innerDb.patch(change.id, { updatedAt: now } as never)
|
|
121
|
-
}
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
}
|
package/src/zod-table.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ConvexValidatorFromZod,
|
|
3
|
-
zid,
|
|
4
|
-
zodToConvex,
|
|
5
|
-
} from 'convex-helpers/server/zod4'
|
|
6
|
-
import { defineTable, type TableDefinition } from 'convex/server'
|
|
7
|
-
import type { GenericId } from 'convex/values'
|
|
8
|
-
import { z } from 'zod'
|
|
9
|
-
|
|
10
|
-
type Shape = Record<string, z.ZodType>
|
|
11
|
-
type ShapeKey<Fields extends Shape> = Extract<keyof Fields, string>
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Opinionated lifecycle timestamps — every table has them, server-owned,
|
|
15
|
-
* always excluded from insert/command boundaries. Maintained by the
|
|
16
|
-
* `timestamps` trigger helper (see cvx-kit/triggers).
|
|
17
|
-
*/
|
|
18
|
-
const timestampShape = {
|
|
19
|
-
createdAt: z.number().optional(),
|
|
20
|
-
updatedAt: z.number().optional(),
|
|
21
|
-
archivedAt: z.number().optional(),
|
|
22
|
-
} satisfies Shape
|
|
23
|
-
|
|
24
|
-
type TimestampShape = typeof timestampShape
|
|
25
|
-
|
|
26
|
-
export const TIMESTAMP_FIELDS = [
|
|
27
|
-
'createdAt',
|
|
28
|
-
'updatedAt',
|
|
29
|
-
'archivedAt',
|
|
30
|
-
] as const
|
|
31
|
-
|
|
32
|
-
export type TableBoundaryOptions<
|
|
33
|
-
Fields extends Shape,
|
|
34
|
-
ServerFields extends readonly ShapeKey<Fields>[],
|
|
35
|
-
CommandFields extends readonly ShapeKey<Fields>[],
|
|
36
|
-
PublicFields extends readonly ShapeKey<Fields & TimestampShape>[],
|
|
37
|
-
> = {
|
|
38
|
-
serverFields?: ServerFields
|
|
39
|
-
commandFields?: CommandFields
|
|
40
|
-
publicFields?: PublicFields
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Defines one table and derives its storage, document, write, and DTO boundaries. */
|
|
44
|
-
export function zodTable<
|
|
45
|
-
Table extends string,
|
|
46
|
-
Fields extends Shape,
|
|
47
|
-
const ServerFields extends readonly ShapeKey<Fields>[] = readonly [],
|
|
48
|
-
const CommandFields extends readonly ShapeKey<Fields>[] = readonly [],
|
|
49
|
-
const PublicFields extends readonly ShapeKey<
|
|
50
|
-
Fields & TimestampShape
|
|
51
|
-
>[] = readonly [],
|
|
52
|
-
>(
|
|
53
|
-
tableName: Table,
|
|
54
|
-
fields: (id: typeof zid) => Fields,
|
|
55
|
-
options: TableBoundaryOptions<
|
|
56
|
-
Fields,
|
|
57
|
-
ServerFields,
|
|
58
|
-
CommandFields,
|
|
59
|
-
PublicFields
|
|
60
|
-
> = {},
|
|
61
|
-
) {
|
|
62
|
-
const shape = { ...fields(zid), ...timestampShape } as Fields &
|
|
63
|
-
TimestampShape
|
|
64
|
-
const storage = z.object(shape).strict()
|
|
65
|
-
const schema = storage.extend({
|
|
66
|
-
_id: zid(tableName),
|
|
67
|
-
_creationTime: z.number(),
|
|
68
|
-
})
|
|
69
|
-
const insertSchema = z
|
|
70
|
-
.object(
|
|
71
|
-
omitShape(shape, [
|
|
72
|
-
...TIMESTAMP_FIELDS,
|
|
73
|
-
...(options.serverFields ?? ([] as never)),
|
|
74
|
-
] as never),
|
|
75
|
-
)
|
|
76
|
-
.strict() as z.ZodObject<
|
|
77
|
-
Omit<Fields, ServerFields[number] | (typeof TIMESTAMP_FIELDS)[number]>
|
|
78
|
-
>
|
|
79
|
-
const updateSchema = insertSchema.partial()
|
|
80
|
-
const commandInput = z
|
|
81
|
-
.object(pickShape(shape, options.commandFields ?? ([] as never)))
|
|
82
|
-
.strict()
|
|
83
|
-
const publicDto = z
|
|
84
|
-
.object(pickShape(shape, options.publicFields ?? ([] as never)))
|
|
85
|
-
.strict()
|
|
86
|
-
const toPublicDto = (row: z.input<typeof storage>) =>
|
|
87
|
-
publicDto.parse(
|
|
88
|
-
Object.fromEntries(
|
|
89
|
-
Object.keys(publicDto.shape).map((key) => [
|
|
90
|
-
key,
|
|
91
|
-
(row as Record<string, unknown>)[key],
|
|
92
|
-
]),
|
|
93
|
-
),
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
type InsertSchema = typeof insertSchema
|
|
97
|
-
type UpdateSchema = typeof updateSchema
|
|
98
|
-
type InsertMask = z.util.Mask<keyof InsertSchema['shape']>
|
|
99
|
-
type UpdateMask = z.util.Mask<keyof UpdateSchema['shape']>
|
|
100
|
-
|
|
101
|
-
function insert(): InsertSchema
|
|
102
|
-
function insert<OmitMask extends InsertMask>(
|
|
103
|
-
omit: OmitMask,
|
|
104
|
-
): ZodObjectOmit<InsertSchema, OmitMask>
|
|
105
|
-
function insert(omit?: InsertMask) {
|
|
106
|
-
return omit ? insertSchema.omit(omit as never) : insertSchema
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function update(): UpdateSchema
|
|
110
|
-
function update<OmitMask extends UpdateMask>(
|
|
111
|
-
omit: OmitMask,
|
|
112
|
-
): ZodObjectOmit<UpdateSchema, OmitMask>
|
|
113
|
-
function update(omit?: UpdateMask) {
|
|
114
|
-
return omit ? updateSchema.omit(omit as never) : updateSchema
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
tableName,
|
|
119
|
-
schema,
|
|
120
|
-
storage,
|
|
121
|
-
insertSchema,
|
|
122
|
-
updateSchema,
|
|
123
|
-
commandInput,
|
|
124
|
-
publicDto,
|
|
125
|
-
toPublicDto,
|
|
126
|
-
table: defineTable(zodToConvex(storage)) as TableDefinition<
|
|
127
|
-
ConvexValidatorFromZod<typeof storage, 'required'>
|
|
128
|
-
>,
|
|
129
|
-
insert,
|
|
130
|
-
update,
|
|
131
|
-
tools: {
|
|
132
|
-
insert: commandInput,
|
|
133
|
-
update: z
|
|
134
|
-
.object({ data: commandInput.partial(), id: zid(tableName) })
|
|
135
|
-
.strict(),
|
|
136
|
-
id: z.object({ id: zid(tableName) }).strict(),
|
|
137
|
-
},
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const tenantShape = {
|
|
142
|
-
tenant: z.string().min(1),
|
|
143
|
-
} satisfies Shape
|
|
144
|
-
|
|
145
|
-
type TenantShape = typeof tenantShape
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* A zodTable whose rows are tenant-owned: injects the server-owned `tenant`
|
|
149
|
-
* field (the tenancy boundary — see cvx-kit/tenancy). Like timestamps, it is
|
|
150
|
-
* excluded from insert/update/command boundaries; handlers stamp it from
|
|
151
|
-
* ctx.tenant, row-level security matches on it, and the tenantOwnership
|
|
152
|
-
* trigger forbids reassigning it. Expose it via publicFields only when a DTO
|
|
153
|
-
* genuinely needs it.
|
|
154
|
-
*/
|
|
155
|
-
export function tenantTable<
|
|
156
|
-
Table extends string,
|
|
157
|
-
Fields extends Shape,
|
|
158
|
-
const ServerFields extends readonly ShapeKey<Fields>[] = readonly [],
|
|
159
|
-
const CommandFields extends readonly ShapeKey<Fields>[] = readonly [],
|
|
160
|
-
const PublicFields extends readonly ShapeKey<
|
|
161
|
-
Fields & TenantShape & TimestampShape
|
|
162
|
-
>[] = readonly [],
|
|
163
|
-
>(
|
|
164
|
-
tableName: Table,
|
|
165
|
-
fields: (id: typeof zid) => Fields,
|
|
166
|
-
options: TableBoundaryOptions<
|
|
167
|
-
Fields & TenantShape,
|
|
168
|
-
readonly (ServerFields[number] | 'tenant')[],
|
|
169
|
-
CommandFields,
|
|
170
|
-
PublicFields
|
|
171
|
-
> = {},
|
|
172
|
-
) {
|
|
173
|
-
return zodTable(
|
|
174
|
-
tableName,
|
|
175
|
-
(id) => ({ ...fields(id), ...tenantShape }),
|
|
176
|
-
{
|
|
177
|
-
...options,
|
|
178
|
-
serverFields: [
|
|
179
|
-
...((options.serverFields ?? []) as readonly (
|
|
180
|
-
| ServerFields[number]
|
|
181
|
-
| 'tenant'
|
|
182
|
-
)[]),
|
|
183
|
-
'tenant',
|
|
184
|
-
] as never,
|
|
185
|
-
} as never,
|
|
186
|
-
) as ReturnType<
|
|
187
|
-
typeof zodTable<
|
|
188
|
-
Table,
|
|
189
|
-
Fields & TenantShape,
|
|
190
|
-
readonly (ServerFields[number] | 'tenant')[],
|
|
191
|
-
CommandFields,
|
|
192
|
-
PublicFields
|
|
193
|
-
>
|
|
194
|
-
>
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Merges per-module table maps into the application schema map, rejecting
|
|
199
|
-
* duplicate table names across modules — the module-registry combinator for
|
|
200
|
-
* domain/table.ts: `defineSchema(createModule(catalogTables, salesTables))`.
|
|
201
|
-
*/
|
|
202
|
-
export function createModule<
|
|
203
|
-
const Maps extends readonly Record<string, TableDefinition<any>>[],
|
|
204
|
-
>(...maps: Maps): UnionToIntersection<Maps[number]> {
|
|
205
|
-
const combined: Record<string, TableDefinition<any>> = {}
|
|
206
|
-
for (const map of maps) {
|
|
207
|
-
for (const [tableName, definition] of Object.entries(map)) {
|
|
208
|
-
if (tableName in combined) {
|
|
209
|
-
throw new Error(
|
|
210
|
-
`Table "${tableName}" is declared by more than one module`,
|
|
211
|
-
)
|
|
212
|
-
}
|
|
213
|
-
combined[tableName] = definition
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return combined as UnionToIntersection<Maps[number]>
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
type UnionToIntersection<Union> = (
|
|
220
|
-
Union extends unknown
|
|
221
|
-
? (member: Union) => void
|
|
222
|
-
: never
|
|
223
|
-
) extends (member: infer Intersection) => void
|
|
224
|
-
? Intersection
|
|
225
|
-
: never
|
|
226
|
-
|
|
227
|
-
/** Preserves discriminated-union storage while retaining the same table owner. */
|
|
228
|
-
export function zodVariantTable<Table extends string, Schema extends z.ZodType>(
|
|
229
|
-
tableName: Table,
|
|
230
|
-
storage: Schema,
|
|
231
|
-
) {
|
|
232
|
-
return {
|
|
233
|
-
tableName,
|
|
234
|
-
storage,
|
|
235
|
-
insertSchema: storage,
|
|
236
|
-
table: defineTable(zodToConvex(storage)),
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* A zid that presents as a plain string in generated JSON schemas so
|
|
242
|
-
* LLM-tool inputs stay primitive, while keeping the Id type at compile time.
|
|
243
|
-
*/
|
|
244
|
-
export function jsonSafeZid<Table extends string>(tableName: Table) {
|
|
245
|
-
return z
|
|
246
|
-
.string()
|
|
247
|
-
.describe(
|
|
248
|
-
`Convex document id for table "${tableName}"`,
|
|
249
|
-
) as unknown as z.ZodType<GenericId<Table>, string>
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
type ZodObjectOmit<
|
|
253
|
-
Schema extends z.ZodObject<any, any>,
|
|
254
|
-
OmitMask extends z.util.Mask<keyof Schema['shape']>,
|
|
255
|
-
> =
|
|
256
|
-
Schema extends z.ZodObject<infer ObjectShape, infer Config>
|
|
257
|
-
? z.ZodObject<
|
|
258
|
-
z.util.Flatten<
|
|
259
|
-
Omit<ObjectShape, Extract<keyof ObjectShape, keyof OmitMask>>
|
|
260
|
-
>,
|
|
261
|
-
Config
|
|
262
|
-
>
|
|
263
|
-
: never
|
|
264
|
-
|
|
265
|
-
function pickShape<
|
|
266
|
-
Fields extends Shape,
|
|
267
|
-
const Keys extends readonly ShapeKey<Fields>[],
|
|
268
|
-
>(fields: Fields, keys: Keys) {
|
|
269
|
-
return Object.fromEntries(keys.map((key) => [key, fields[key]])) as Pick<
|
|
270
|
-
Fields,
|
|
271
|
-
Keys[number]
|
|
272
|
-
>
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function omitShape<
|
|
276
|
-
Fields extends Shape,
|
|
277
|
-
const Keys extends readonly ShapeKey<Fields>[],
|
|
278
|
-
>(fields: Fields, keys: Keys) {
|
|
279
|
-
const omitted = new Set<string>(keys)
|
|
280
|
-
return Object.fromEntries(
|
|
281
|
-
Object.entries(fields).filter(([key]) => !omitted.has(key)),
|
|
282
|
-
) as Omit<Fields, Keys[number]>
|
|
283
|
-
}
|