Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/property-definition-navigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@cleverbrush/knex-schema': patch
'@cleverbrush/orm': patch
'@cleverbrush/mapper': patch
'@cleverbrush/schema-json': patch
'@cleverbrush/server': patch
'@cleverbrush/deep': patch
---

Preserve original property declarations and JSDoc through derived types so
editors can navigate to definitions and show property documentation. This covers
query selectors, rows, projections and write payloads; declared relation includes;
mapper targets; JSON Schema inferred values; composed API groups and injected
services; and merged object properties. Runtime behavior is unchanged.
10 changes: 9 additions & 1 deletion libs/deep/src/deepExtend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,17 @@ type DeepSafeMergeProps<T> = {
[K in keyof SafeMergeProps<T>]: DeepSafeMergeValue<SafeProp<T, K>>;
};

type CommonSource<T1, T2> = Pick<
SafeMergeProps<T2>,
keyof SafeMergeProps<T1> & keyof SafeMergeProps<T2>
>;

/** Properties that exist in both `T1` and `T2`, typed as `T2`'s version. */
export type CommonProps<T1, T2> = {
[k in keyof SafeMergeProps<T1> & keyof SafeMergeProps<T2>]: SafeProp<
// Shared values come from T2; retain that property's declaration and JSDoc.
// Required makes the slots required without removing undefined from T2's
// optional values when they are read through SafeProp below.
-readonly [k in keyof Required<CommonSource<T1, T2>>]: SafeProp<
T1,
k
> extends never
Expand Down
33 changes: 24 additions & 9 deletions libs/knex-schema/src/PolymorphicQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ type BranchProps<S extends ReadObject, K extends keyof VariantMap<S>> = Omit<
keyof ReadRelations<VariantBody<S, K>> | VariantKey<S, K>
>;
type OrphanProps<P, V> = {
[K in keyof P & string]: V extends { allowOrphan: true }
-readonly [K in keyof P as K extends string ? K : never]-?: V extends {
allowOrphan: true;
}
? SchemaForValue<InferType<P[K]> | null>
: P[K] extends ReadSchema
? P[K]
Expand All @@ -95,12 +97,21 @@ export type VariantReadSchema<
VariantKey<S, K> | Discriminator<S>
>,
VariantMap<S>[K]
> &
Record<Discriminator<S>, SchemaBuilder<K>>
> & {
-readonly [P in keyof Pick<
SchemaProps<S>,
Extract<Discriminator<S>, keyof SchemaProps<S>>
>]-?: SchemaBuilder<K>;
} & Record<
Exclude<Discriminator<S>, keyof SchemaProps<S>>,
SchemaBuilder<K>
>
>;
/** Per-variant schemas for explicit application-level discriminator dispatch. */
export type VariantReadSchemas<S extends ReadObject> = {
[K in keyof VariantMap<S> & string]: VariantReadSchema<S, K>;
-readonly [K in keyof VariantMap<S> as K extends string
? K
: never]-?: VariantReadSchema<S, K & string>;
};
/** The genuine union schema returned for polymorphic read rows. */
export type PolymorphicRowSchema<B extends Record<string, ReadObject>> =
Expand Down Expand Up @@ -531,11 +542,15 @@ export class PolymorphicQueryBuilder<
S,
{
[P in keyof B]: ObjectSchemaBuilder<
SchemaProps<B[P]> &
Record<
K,
RelationField<ReadRelations<S>[K], Child['rowSchema']>
>
SchemaProps<B[P]> & {
-readonly [R in keyof Pick<
ReadRelations<S>,
K
>]-?: RelationField<
ReadRelations<S>[K],
Child['rowSchema']
>;
}
>;
}
> {
Expand Down
52 changes: 35 additions & 17 deletions libs/knex-schema/src/SchemaQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,21 @@ export interface ReadColumn<S extends ReadSchema, K extends string = string>
/** Type-only property identity for column-list projections. */
readonly __property?: K;
}
// Map directly over source properties and filter in `as` so TypeScript retains
// their declarations/JSDoc. Mapping a computed key union loses those origins.
// Explicit modifiers keep the existing required, mutable column slots.
/** A JSON object column also exposes its known nested properties. */
export type NestedReadColumn<S, Key extends string = string> = ReadColumn<
ColumnReadSchema<S>,
Key
> &
(S extends ReadObject
? {
[K in keyof SchemaProps<S> & string]: NestedReadColumn<
-readonly [K in keyof SchemaProps<S> as K extends string
? K
: never]-?: NestedReadColumn<
SchemaProps<S>[K],
`${Key}.${K}`
`${Key}.${K & string}`
>;
}
: {});
Expand All @@ -112,10 +117,11 @@ export type ReadColumns<
S extends ReadObject,
Relations extends PropertyKey = never
> = {
[K in Exclude<keyof SchemaProps<S>, Relations> & string]: NestedReadColumn<
SchemaProps<S>[K],
K
>;
-readonly [K in keyof SchemaProps<S> as K extends Relations
? never
: K extends string
? K
: never]-?: NestedReadColumn<SchemaProps<S>[K], K & string>;
};
type SelectedValue<Columns, Selector> =
| (Selector extends (...args: any[]) => AliasedColumn<infer Value>
Expand All @@ -127,13 +133,10 @@ type SelectedValue<Columns, Selector> =
: never)
| null;
type Selection = Record<string, ReadColumn<any> | AggregateExpression<any>>;
// Keep each side's property origins while retaining the right side's precedence.
type MergeProps<A, B> = {
[K in keyof A | keyof B]: K extends keyof B
? B[K]
: K extends keyof A
? A[K]
: never;
};
-readonly [K in keyof Required<A> as K extends keyof B ? never : K]: A[K];
} & { -readonly [K in keyof Required<B>]: B[K] };
type NamedProjections<S> = S extends { readonly [EXTRA_TYPE_BRAND]?: infer P }
? P
: {};
Expand All @@ -145,7 +148,9 @@ type NamedKeys<
: never;
/** Structural schema inferred from a typed projection. */
export type ReadProjection<S extends Selection> = ObjectSchemaBuilder<{
[K in keyof S & string]: S[K] extends ReadColumn<infer R>
-readonly [K in keyof S as K extends string
? K
: never]-?: S[K] extends ReadColumn<infer R>
? R
: S[K] extends AggregateExpression<infer T>
? SchemaForValue<T>
Expand All @@ -154,8 +159,14 @@ export type ReadProjection<S extends Selection> = ObjectSchemaBuilder<{
type AddField<
S extends ReadObject,
K extends string,
F extends ReadSchema
> = ObjectSchemaBuilder<Omit<SchemaProps<S>, K> & Record<K, F>>;
F extends ReadSchema,
Origin = SchemaProps<S>
> = ObjectSchemaBuilder<
// Declared includes have an origin; ad-hoc join aliases may introduce a key.
Omit<SchemaProps<S>, K> & {
-readonly [P in keyof Pick<Origin, Extract<K, keyof Origin>>]-?: F;
} & Record<Exclude<K, keyof Origin>, F>
>;
/** @internal Foreign schema retained by a declared relation. */
export type Related<R> = R extends RelationInfo<any, infer S> ? S : never;
/** @internal Output cardinality and nullability of a loaded relation. */
Expand Down Expand Up @@ -376,7 +387,9 @@ export class SchemaQueryBuilder<
'Scopes must synchronously return the supplied query with filters, ordering or pagination only'
);
}
return result as this;
// The runtime checks above preserve this query's shape; instanceof
// alone cannot recover its schema and relation type parameters.
return result as unknown as this;
}

/** Apply a named, synchronous shape-preserving scope once to an independent query. */
Expand Down Expand Up @@ -956,7 +969,12 @@ export class SchemaQueryBuilder<
customize?: (query: SchemaAwareQuery<Related<Relations[K]>>) => Child
): SchemaQueryBuilder<
S,
AddField<Row, K, RelationField<Relations[K], Child['rowSchema']>>,
AddField<
Row,
K,
RelationField<Relations[K], Child['rowSchema']>,
Relations
>,
Relations,
false
> {
Expand Down
24 changes: 18 additions & 6 deletions libs/knex-schema/src/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,16 @@ export type WithRelation<
TReadVariants extends ReadVariants = {}
> = Entity<
TSchema,
TRels & Record<TKey, RelationInfo<TKind, TForeign>>,
// Derive metadata from schema properties so includes retain their origins.
TRels & {
-readonly [P in keyof Pick<
SchemaProps<TSchema>,
Extract<TKey, keyof SchemaProps<TSchema>>
>]-?: RelationInfo<TKind, TForeign>;
} & Record<
Exclude<TKey, keyof SchemaProps<TSchema>>,
RelationInfo<TKind, TForeign>
>,
TVariantUnion,
TReadVariants
>;
Expand Down Expand Up @@ -377,11 +386,14 @@ export class Entity<
opts?: { optional?: TOptional }
): Entity<
TSchema,
TRels &
Record<
TKey,
RelationInfo<'belongsTo', TForeign> & { optional: TOptional }
>,
TRels & {
-readonly [P in keyof Pick<
SchemaProps<TSchema>,
TKey
>]-?: RelationInfo<'belongsTo', TForeign> & {
optional: TOptional;
};
},
TVariantUnion,
TReadVariants
> {
Expand Down
Loading
Loading