@prisma-next/sql-orm-client 0.7.0 → 0.8.0
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/index.d.mts +70 -17
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +210 -41
- package/dist/index.mjs.map +1 -1
- package/package.json +21 -21
- package/src/collection.ts +262 -56
- package/src/grouped-collection.ts +34 -8
- package/src/query-plan-meta.ts +57 -3
- package/src/query-plan-select.ts +4 -3
- package/src/query-plan.ts +1 -0
- package/src/types.ts +10 -0
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-orm-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "ORM client for Prisma Next — fluent, type-safe model collections",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/contract": "0.
|
|
10
|
-
"@prisma-next/framework-components": "0.
|
|
11
|
-
"@prisma-next/operations": "0.
|
|
12
|
-
"@prisma-next/sql-contract": "0.
|
|
13
|
-
"@prisma-next/sql-operations": "0.
|
|
14
|
-
"@prisma-next/
|
|
15
|
-
"@prisma-next/sql-runtime": "0.
|
|
16
|
-
"@prisma-next/
|
|
9
|
+
"@prisma-next/contract": "0.8.0",
|
|
10
|
+
"@prisma-next/framework-components": "0.8.0",
|
|
11
|
+
"@prisma-next/operations": "0.8.0",
|
|
12
|
+
"@prisma-next/sql-contract": "0.8.0",
|
|
13
|
+
"@prisma-next/sql-operations": "0.8.0",
|
|
14
|
+
"@prisma-next/sql-relational-core": "0.8.0",
|
|
15
|
+
"@prisma-next/sql-runtime": "0.8.0",
|
|
16
|
+
"@prisma-next/utils": "0.8.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
+
"@prisma-next/adapter-postgres": "0.8.0",
|
|
20
|
+
"@prisma-next/driver-postgres": "0.8.0",
|
|
21
|
+
"@prisma-next/cli": "0.8.0",
|
|
22
|
+
"@prisma-next/extension-pgvector": "0.8.0",
|
|
23
|
+
"@prisma-next/family-sql": "0.8.0",
|
|
24
|
+
"@prisma-next/ids": "0.8.0",
|
|
25
|
+
"@prisma-next/sql-contract-ts": "0.8.0",
|
|
26
|
+
"@prisma-next/target-postgres": "0.8.0",
|
|
27
|
+
"@prisma-next/test-utils": "0.8.0",
|
|
28
|
+
"@prisma-next/tsconfig": "0.8.0",
|
|
29
|
+
"@prisma-next/tsdown": "0.8.0",
|
|
19
30
|
"@types/pg": "8.20.0",
|
|
20
31
|
"pg": "8.20.0",
|
|
21
32
|
"tsdown": "0.22.0",
|
|
22
33
|
"typescript": "5.9.3",
|
|
23
|
-
"vitest": "4.1.5"
|
|
24
|
-
"@prisma-next/adapter-postgres": "0.7.0",
|
|
25
|
-
"@prisma-next/cli": "0.7.0",
|
|
26
|
-
"@prisma-next/driver-postgres": "0.7.0",
|
|
27
|
-
"@prisma-next/extension-pgvector": "0.7.0",
|
|
28
|
-
"@prisma-next/family-sql": "0.7.0",
|
|
29
|
-
"@prisma-next/ids": "0.7.0",
|
|
30
|
-
"@prisma-next/sql-contract-ts": "0.7.0",
|
|
31
|
-
"@prisma-next/target-postgres": "0.7.0",
|
|
32
|
-
"@prisma-next/test-utils": "0.7.0",
|
|
33
|
-
"@prisma-next/tsconfig": "0.7.0",
|
|
34
|
-
"@prisma-next/tsdown": "0.7.0"
|
|
34
|
+
"vitest": "4.1.5"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|
package/src/collection.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
-
import {
|
|
2
|
+
import type {
|
|
3
|
+
AnnotationValue,
|
|
4
|
+
MetaBuilder,
|
|
5
|
+
OperationKind,
|
|
6
|
+
} from '@prisma-next/framework-components/runtime';
|
|
7
|
+
import { AsyncIterableResult, createMetaBuilder } from '@prisma-next/framework-components/runtime';
|
|
3
8
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
4
9
|
import {
|
|
5
10
|
type AnyExpression,
|
|
@@ -85,6 +90,7 @@ import {
|
|
|
85
90
|
compileUpdateCount,
|
|
86
91
|
compileUpdateReturning,
|
|
87
92
|
compileUpsertReturning,
|
|
93
|
+
mergeAnnotations,
|
|
88
94
|
} from './query-plan';
|
|
89
95
|
import {
|
|
90
96
|
type AggregateBuilder,
|
|
@@ -642,19 +648,51 @@ export class Collection<
|
|
|
642
648
|
return this.#clone({ offset: n });
|
|
643
649
|
}
|
|
644
650
|
|
|
645
|
-
|
|
646
|
-
|
|
651
|
+
/**
|
|
652
|
+
* Read terminal: stream all rows matching the current state.
|
|
653
|
+
*
|
|
654
|
+
* Accepts an optional `configure` callback that receives a
|
|
655
|
+
* `MetaBuilder<'read'>` so the caller can attach typed user
|
|
656
|
+
* annotations to the executed plan. `meta.annotate(...)` enforces
|
|
657
|
+
* applicability at the type level and at runtime; annotations are
|
|
658
|
+
* merged into `plan.meta.annotations` at compile time.
|
|
659
|
+
*/
|
|
660
|
+
all(configure?: (meta: MetaBuilder<'read'>) => void): AsyncIterableResult<Row> {
|
|
661
|
+
return this.#withAnnotationsFromMeta(configure, 'all').#dispatch();
|
|
647
662
|
}
|
|
648
663
|
|
|
649
664
|
async first(): Promise<Row | null>;
|
|
665
|
+
async first(
|
|
666
|
+
filter: undefined,
|
|
667
|
+
configure: (meta: MetaBuilder<'read'>) => void,
|
|
668
|
+
): Promise<Row | null>;
|
|
650
669
|
async first(
|
|
651
670
|
filter: (model: ModelAccessor<TContract, ModelName>) => WhereArg,
|
|
671
|
+
configure?: (meta: MetaBuilder<'read'>) => void,
|
|
652
672
|
): Promise<Row | null>;
|
|
653
|
-
async first(
|
|
673
|
+
async first(
|
|
674
|
+
filter: ShorthandWhereFilter<TContract, ModelName>,
|
|
675
|
+
configure?: (meta: MetaBuilder<'read'>) => void,
|
|
676
|
+
): Promise<Row | null>;
|
|
677
|
+
/**
|
|
678
|
+
* Read terminal: return the first matching row, or `null`.
|
|
679
|
+
*
|
|
680
|
+
* Accepts an optional `filter` (function or shorthand) followed by an
|
|
681
|
+
* optional `configure` callback that receives a `MetaBuilder<'read'>`
|
|
682
|
+
* for attaching typed annotations. To attach annotations without
|
|
683
|
+
* narrowing further, pass `undefined` as the filter (or chain
|
|
684
|
+
* `.where(...)` first):
|
|
685
|
+
*
|
|
686
|
+
* ```typescript
|
|
687
|
+
* await db.User.first({ id }, (meta) => meta.annotate(cacheAnnotation({ ttl: 60 })));
|
|
688
|
+
* await db.User.first(undefined, (meta) => meta.annotate(cacheAnnotation({ ttl: 60 })));
|
|
689
|
+
* ```
|
|
690
|
+
*/
|
|
654
691
|
async first(
|
|
655
692
|
filter?:
|
|
656
693
|
| ((model: ModelAccessor<TContract, ModelName>) => WhereArg)
|
|
657
694
|
| ShorthandWhereFilter<TContract, ModelName>,
|
|
695
|
+
configure?: (meta: MetaBuilder<'read'>) => void,
|
|
658
696
|
): Promise<Row | null> {
|
|
659
697
|
const scoped =
|
|
660
698
|
filter === undefined
|
|
@@ -662,13 +700,22 @@ export class Collection<
|
|
|
662
700
|
: typeof filter === 'function'
|
|
663
701
|
? this.where(filter)
|
|
664
702
|
: this.where(filter);
|
|
665
|
-
const limited = scoped.take(1);
|
|
703
|
+
const limited = scoped.take(1).#withAnnotationsFromMeta(configure, 'first');
|
|
666
704
|
const rows = await limited.#dispatch().toArray();
|
|
667
705
|
return rows[0] ?? null;
|
|
668
706
|
}
|
|
669
707
|
|
|
708
|
+
/**
|
|
709
|
+
* Read terminal: run an aggregate query (count, sum, avg, min, max)
|
|
710
|
+
* built via the `AggregateBuilder` callback.
|
|
711
|
+
*
|
|
712
|
+
* Accepts an optional `configure` callback that receives a
|
|
713
|
+
* `MetaBuilder<'read'>` for attaching typed annotations.
|
|
714
|
+
* Annotations are merged into the compiled plan's `meta.annotations`.
|
|
715
|
+
*/
|
|
670
716
|
async aggregate<Spec extends AggregateSpec>(
|
|
671
717
|
fn: (aggregate: AggregateBuilder<TContract, ModelName>) => Spec,
|
|
718
|
+
configure?: (meta: MetaBuilder<'read'>) => void,
|
|
672
719
|
): Promise<AggregateResult<Spec>> {
|
|
673
720
|
const aggregateSpec = fn(createAggregateBuilder(this.contract, this.modelName));
|
|
674
721
|
const entries = Object.entries(aggregateSpec);
|
|
@@ -682,11 +729,11 @@ export class Collection<
|
|
|
682
729
|
}
|
|
683
730
|
}
|
|
684
731
|
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
this.state.filters,
|
|
689
|
-
|
|
732
|
+
const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'read', 'aggregate');
|
|
733
|
+
|
|
734
|
+
const compiled = mergeAnnotations(
|
|
735
|
+
compileAggregate(this.contract, this.tableName, this.state.filters, aggregateSpec),
|
|
736
|
+
annotationsMap,
|
|
690
737
|
);
|
|
691
738
|
const rows = await executeQueryPlan<Record<string, unknown>>(
|
|
692
739
|
this.ctx.runtime,
|
|
@@ -695,14 +742,36 @@ export class Collection<
|
|
|
695
742
|
return normalizeAggregateResult(aggregateSpec, rows[0] ?? {});
|
|
696
743
|
}
|
|
697
744
|
|
|
698
|
-
async create(
|
|
699
|
-
|
|
745
|
+
async create(
|
|
746
|
+
data: ResolvedCreateInput<TContract, ModelName, State['variantName']>,
|
|
747
|
+
configure?: (meta: MetaBuilder<'write'>) => void,
|
|
748
|
+
): Promise<Row>;
|
|
749
|
+
async create(
|
|
750
|
+
data: MutationCreateInputWithRelations<TContract, ModelName>,
|
|
751
|
+
configure?: (meta: MetaBuilder<'write'>) => void,
|
|
752
|
+
): Promise<Row>;
|
|
753
|
+
/**
|
|
754
|
+
* Write terminal: insert one row and return it.
|
|
755
|
+
*
|
|
756
|
+
* Accepts an optional `configure` callback that receives a
|
|
757
|
+
* `MetaBuilder<'write'>` for attaching typed annotations.
|
|
758
|
+
* Annotations are merged into the compiled mutation plan's
|
|
759
|
+
* `meta.annotations`.
|
|
760
|
+
*
|
|
761
|
+
* Note: when the input contains nested-mutation callbacks, the
|
|
762
|
+
* operation is executed as a graph of internal queries via
|
|
763
|
+
* `withMutationScope`. In that path, annotations apply to the
|
|
764
|
+
* logical `create()` call but do not currently flow into each
|
|
765
|
+
* constituent SQL statement issued for the related rows.
|
|
766
|
+
*/
|
|
700
767
|
async create(
|
|
701
768
|
data:
|
|
702
769
|
| ResolvedCreateInput<TContract, ModelName, State['variantName']>
|
|
703
770
|
| MutationCreateInputWithRelations<TContract, ModelName>,
|
|
771
|
+
configure?: (meta: MetaBuilder<'write'>) => void,
|
|
704
772
|
): Promise<Row> {
|
|
705
773
|
assertReturningCapability(this.contract, 'create()');
|
|
774
|
+
const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'create');
|
|
706
775
|
|
|
707
776
|
if (
|
|
708
777
|
hasNestedMutationCallbacks(this.contract, this.modelName, data as Record<string, unknown>)
|
|
@@ -722,9 +791,10 @@ export class Collection<
|
|
|
722
791
|
return reloaded;
|
|
723
792
|
}
|
|
724
793
|
|
|
725
|
-
const rows = await this
|
|
726
|
-
data as ResolvedCreateInput<TContract, ModelName, State['variantName']
|
|
727
|
-
|
|
794
|
+
const rows = await this.#createAllWithAnnotations(
|
|
795
|
+
[data as ResolvedCreateInput<TContract, ModelName, State['variantName']>],
|
|
796
|
+
annotationsMap,
|
|
797
|
+
);
|
|
728
798
|
const created = rows[0];
|
|
729
799
|
if (created) {
|
|
730
800
|
return created;
|
|
@@ -735,6 +805,17 @@ export class Collection<
|
|
|
735
805
|
|
|
736
806
|
createAll(
|
|
737
807
|
data: readonly ResolvedCreateInput<TContract, ModelName, State['variantName']>[],
|
|
808
|
+
configure?: (meta: MetaBuilder<'write'>) => void,
|
|
809
|
+
): AsyncIterableResult<Row> {
|
|
810
|
+
return this.#createAllWithAnnotations(
|
|
811
|
+
data,
|
|
812
|
+
this.#collectAnnotationsFromMeta(configure, 'write', 'createAll'),
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
#createAllWithAnnotations(
|
|
817
|
+
data: readonly ResolvedCreateInput<TContract, ModelName, State['variantName']>[],
|
|
818
|
+
annotationsMap: ReadonlyMap<string, AnnotationValue<unknown, OperationKind>> | undefined,
|
|
738
819
|
): AsyncIterableResult<Row> {
|
|
739
820
|
if (data.length === 0) {
|
|
740
821
|
const generator = async function* (): AsyncGenerator<Row, void, unknown> {};
|
|
@@ -762,7 +843,7 @@ export class Collection<
|
|
|
762
843
|
this.tableName,
|
|
763
844
|
mappedRows,
|
|
764
845
|
selectedForInsert,
|
|
765
|
-
);
|
|
846
|
+
).map((plan) => mergeAnnotations(plan, annotationsMap));
|
|
766
847
|
return dispatchSplitMutationRows<Row>({
|
|
767
848
|
contract: this.contract,
|
|
768
849
|
runtime: this.ctx.runtime,
|
|
@@ -774,11 +855,9 @@ export class Collection<
|
|
|
774
855
|
});
|
|
775
856
|
}
|
|
776
857
|
|
|
777
|
-
const compiled =
|
|
778
|
-
this.contract,
|
|
779
|
-
|
|
780
|
-
mappedRows,
|
|
781
|
-
selectedForInsert,
|
|
858
|
+
const compiled = mergeAnnotations(
|
|
859
|
+
compileInsertReturning(this.contract, this.tableName, mappedRows, selectedForInsert),
|
|
860
|
+
annotationsMap,
|
|
782
861
|
);
|
|
783
862
|
return dispatchMutationRows<Row>({
|
|
784
863
|
contract: this.contract,
|
|
@@ -947,26 +1026,33 @@ export class Collection<
|
|
|
947
1026
|
|
|
948
1027
|
async createCount(
|
|
949
1028
|
data: readonly ResolvedCreateInput<TContract, ModelName, State['variantName']>[],
|
|
1029
|
+
configure?: (meta: MetaBuilder<'write'>) => void,
|
|
950
1030
|
): Promise<number> {
|
|
951
1031
|
if (data.length === 0) {
|
|
952
1032
|
return 0;
|
|
953
1033
|
}
|
|
954
1034
|
|
|
955
1035
|
this.#assertNotMtiVariant('createCount()');
|
|
1036
|
+
const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'createCount');
|
|
956
1037
|
|
|
957
1038
|
const rows = data as readonly Record<string, unknown>[];
|
|
958
1039
|
const mappedRows = this.#mapCreateRows(rows);
|
|
959
1040
|
applyCreateDefaults(this.ctx, this.tableName, mappedRows);
|
|
960
1041
|
|
|
961
1042
|
if (this.contract.capabilities?.['sql']?.['defaultInInsert'] !== true) {
|
|
962
|
-
const plans = compileInsertCountSplit(this.contract, this.tableName, mappedRows)
|
|
1043
|
+
const plans = compileInsertCountSplit(this.contract, this.tableName, mappedRows).map((plan) =>
|
|
1044
|
+
mergeAnnotations(plan, annotationsMap),
|
|
1045
|
+
);
|
|
963
1046
|
for (const plan of plans) {
|
|
964
1047
|
await executeQueryPlan<Record<string, unknown>>(this.ctx.runtime, plan).toArray();
|
|
965
1048
|
}
|
|
966
1049
|
return data.length;
|
|
967
1050
|
}
|
|
968
1051
|
|
|
969
|
-
const compiled =
|
|
1052
|
+
const compiled = mergeAnnotations(
|
|
1053
|
+
compileInsertCount(this.contract, this.tableName, mappedRows),
|
|
1054
|
+
annotationsMap,
|
|
1055
|
+
);
|
|
970
1056
|
await executeQueryPlan<Record<string, unknown>>(this.ctx.runtime, compiled).toArray();
|
|
971
1057
|
return data.length;
|
|
972
1058
|
}
|
|
@@ -976,13 +1062,17 @@ export class Collection<
|
|
|
976
1062
|
* On conflict, `ON CONFLICT DO NOTHING RETURNING ...` may return zero rows,
|
|
977
1063
|
* so this method may issue a follow-up reload query to return the existing row.
|
|
978
1064
|
*/
|
|
979
|
-
async upsert(
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1065
|
+
async upsert(
|
|
1066
|
+
input: {
|
|
1067
|
+
create: ResolvedCreateInput<TContract, ModelName, State['variantName']>;
|
|
1068
|
+
update: Partial<DefaultModelRow<TContract, ModelName>>;
|
|
1069
|
+
conflictOn?: UniqueConstraintCriterion<TContract, ModelName>;
|
|
1070
|
+
},
|
|
1071
|
+
configure?: (meta: MetaBuilder<'write'>) => void,
|
|
1072
|
+
): Promise<Row> {
|
|
984
1073
|
assertReturningCapability(this.contract, 'upsert()');
|
|
985
1074
|
this.#assertNotMtiVariant('upsert()');
|
|
1075
|
+
const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'upsert');
|
|
986
1076
|
|
|
987
1077
|
const mappedCreateRows = this.#mapCreateRows([input.create as Record<string, unknown>]);
|
|
988
1078
|
const createValues = mappedCreateRows[0] ?? {};
|
|
@@ -1006,13 +1096,16 @@ export class Collection<
|
|
|
1006
1096
|
this.state.selectedFields,
|
|
1007
1097
|
parentJoinColumns,
|
|
1008
1098
|
);
|
|
1009
|
-
const compiled =
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1099
|
+
const compiled = mergeAnnotations(
|
|
1100
|
+
compileUpsertReturning(
|
|
1101
|
+
this.contract,
|
|
1102
|
+
this.tableName,
|
|
1103
|
+
createValues,
|
|
1104
|
+
updateValues,
|
|
1105
|
+
conflictColumns,
|
|
1106
|
+
selectedForUpsert,
|
|
1107
|
+
),
|
|
1108
|
+
annotationsMap,
|
|
1016
1109
|
);
|
|
1017
1110
|
const row = await executeMutationReturningSingleRow<Row>({
|
|
1018
1111
|
contract: this.contract,
|
|
@@ -1042,10 +1135,25 @@ export class Collection<
|
|
|
1042
1135
|
throw new Error(`upsert() for model "${this.modelName}" did not return a row`);
|
|
1043
1136
|
}
|
|
1044
1137
|
|
|
1138
|
+
/**
|
|
1139
|
+
* Write terminal: update matching rows and return the first one (or
|
|
1140
|
+
* null when no row matched).
|
|
1141
|
+
*
|
|
1142
|
+
* Accepts an optional `configure` callback that receives a
|
|
1143
|
+
* `MetaBuilder<'write'>` for attaching typed annotations.
|
|
1144
|
+
*
|
|
1145
|
+
* Note: when the input contains nested-mutation callbacks, the
|
|
1146
|
+
* operation is executed as a graph of internal queries via
|
|
1147
|
+
* `withMutationScope`. In that path, annotations apply to the logical
|
|
1148
|
+
* `update()` call but do not currently flow into each constituent SQL
|
|
1149
|
+
* statement issued for the related rows.
|
|
1150
|
+
*/
|
|
1045
1151
|
async update(
|
|
1046
1152
|
data: State['hasWhere'] extends true ? MutationUpdateInput<TContract, ModelName> : never,
|
|
1153
|
+
configure?: (meta: MetaBuilder<'write'>) => void,
|
|
1047
1154
|
): Promise<Row | null> {
|
|
1048
1155
|
assertReturningCapability(this.contract, 'update()');
|
|
1156
|
+
const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'update');
|
|
1049
1157
|
|
|
1050
1158
|
if (
|
|
1051
1159
|
hasNestedMutationCallbacks(this.contract, this.modelName, data as Record<string, unknown>)
|
|
@@ -1072,10 +1180,11 @@ export class Collection<
|
|
|
1072
1180
|
return null;
|
|
1073
1181
|
}
|
|
1074
1182
|
const narrowed = scoped.#clone({ filters: [identityWhere] });
|
|
1075
|
-
const rows = await narrowed
|
|
1183
|
+
const rows = await narrowed.#updateAllWithAnnotations(
|
|
1076
1184
|
data as State['hasWhere'] extends true
|
|
1077
1185
|
? Partial<DefaultModelRow<TContract, ModelName>>
|
|
1078
1186
|
: never,
|
|
1187
|
+
annotationsMap,
|
|
1079
1188
|
);
|
|
1080
1189
|
return rows[0] ?? null;
|
|
1081
1190
|
});
|
|
@@ -1083,6 +1192,17 @@ export class Collection<
|
|
|
1083
1192
|
|
|
1084
1193
|
updateAll(
|
|
1085
1194
|
data: State['hasWhere'] extends true ? Partial<DefaultModelRow<TContract, ModelName>> : never,
|
|
1195
|
+
configure?: (meta: MetaBuilder<'write'>) => void,
|
|
1196
|
+
): AsyncIterableResult<Row> {
|
|
1197
|
+
return this.#updateAllWithAnnotations(
|
|
1198
|
+
data,
|
|
1199
|
+
this.#collectAnnotationsFromMeta(configure, 'write', 'updateAll'),
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
#updateAllWithAnnotations(
|
|
1204
|
+
data: State['hasWhere'] extends true ? Partial<DefaultModelRow<TContract, ModelName>> : never,
|
|
1205
|
+
annotationsMap: ReadonlyMap<string, AnnotationValue<unknown, OperationKind>> | undefined,
|
|
1086
1206
|
): AsyncIterableResult<Row> {
|
|
1087
1207
|
assertReturningCapability(this.contract, 'updateAll()');
|
|
1088
1208
|
|
|
@@ -1099,12 +1219,15 @@ export class Collection<
|
|
|
1099
1219
|
this.state.selectedFields,
|
|
1100
1220
|
parentJoinColumns,
|
|
1101
1221
|
);
|
|
1102
|
-
const compiled =
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1222
|
+
const compiled = mergeAnnotations(
|
|
1223
|
+
compileUpdateReturning(
|
|
1224
|
+
this.contract,
|
|
1225
|
+
this.tableName,
|
|
1226
|
+
mappedData,
|
|
1227
|
+
this.state.filters,
|
|
1228
|
+
selectedForUpdate,
|
|
1229
|
+
),
|
|
1230
|
+
annotationsMap,
|
|
1108
1231
|
);
|
|
1109
1232
|
return dispatchMutationRows<Row>({
|
|
1110
1233
|
contract: this.contract,
|
|
@@ -1119,6 +1242,7 @@ export class Collection<
|
|
|
1119
1242
|
|
|
1120
1243
|
async updateCount(
|
|
1121
1244
|
data: State['hasWhere'] extends true ? Partial<DefaultModelRow<TContract, ModelName>> : never,
|
|
1245
|
+
configure?: (meta: MetaBuilder<'write'>) => void,
|
|
1122
1246
|
): Promise<number> {
|
|
1123
1247
|
const mappedData = mapModelDataToStorageRow(this.contract, this.modelName, data);
|
|
1124
1248
|
if (Object.keys(mappedData).length === 0) {
|
|
@@ -1127,6 +1251,9 @@ export class Collection<
|
|
|
1127
1251
|
|
|
1128
1252
|
applyUpdateDefaults(this.ctx, this.tableName, mappedData);
|
|
1129
1253
|
|
|
1254
|
+
// Annotations attach to the write, not the matching read.
|
|
1255
|
+
const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'updateCount');
|
|
1256
|
+
|
|
1130
1257
|
const primaryKeyColumn = resolvePrimaryKeyColumn(this.contract, this.tableName);
|
|
1131
1258
|
const countState: CollectionState = {
|
|
1132
1259
|
...emptyState(),
|
|
@@ -1139,21 +1266,28 @@ export class Collection<
|
|
|
1139
1266
|
countCompiled,
|
|
1140
1267
|
).toArray();
|
|
1141
1268
|
|
|
1142
|
-
const compiled =
|
|
1143
|
-
this.contract,
|
|
1144
|
-
|
|
1145
|
-
mappedData,
|
|
1146
|
-
this.state.filters,
|
|
1269
|
+
const compiled = mergeAnnotations(
|
|
1270
|
+
compileUpdateCount(this.contract, this.tableName, mappedData, this.state.filters),
|
|
1271
|
+
annotationsMap,
|
|
1147
1272
|
);
|
|
1148
1273
|
await executeQueryPlan<Record<string, unknown>>(this.ctx.runtime, compiled).toArray();
|
|
1149
1274
|
|
|
1150
1275
|
return matchingRows.length;
|
|
1151
1276
|
}
|
|
1152
1277
|
|
|
1278
|
+
/**
|
|
1279
|
+
* Write terminal: delete matching rows and return the first one (or
|
|
1280
|
+
* null when no row matched).
|
|
1281
|
+
*
|
|
1282
|
+
* Accepts an optional `configure` callback that receives a
|
|
1283
|
+
* `MetaBuilder<'write'>` for attaching typed annotations.
|
|
1284
|
+
*/
|
|
1153
1285
|
async delete(
|
|
1154
1286
|
this: State['hasWhere'] extends true ? Collection<TContract, ModelName, Row, State> : never,
|
|
1287
|
+
configure?: (meta: MetaBuilder<'write'>) => void,
|
|
1155
1288
|
): Promise<Row | null> {
|
|
1156
1289
|
assertReturningCapability(this.contract, 'delete()');
|
|
1290
|
+
const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'delete');
|
|
1157
1291
|
return withMutationScope(this.ctx.runtime, async (scope) => {
|
|
1158
1292
|
const scoped = this.#withRuntime(scope);
|
|
1159
1293
|
const identityWhere = await scoped.#findFirstMatchingRowIdentityWhere();
|
|
@@ -1161,29 +1295,38 @@ export class Collection<
|
|
|
1161
1295
|
return null;
|
|
1162
1296
|
}
|
|
1163
1297
|
const narrowed = scoped.#clone({ filters: [identityWhere] });
|
|
1164
|
-
const rows = await narrowed.#executeDeleteReturning().toArray();
|
|
1298
|
+
const rows = await narrowed.#executeDeleteReturning(annotationsMap).toArray();
|
|
1165
1299
|
return rows[0] ?? null;
|
|
1166
1300
|
});
|
|
1167
1301
|
}
|
|
1168
1302
|
|
|
1169
1303
|
deleteAll(
|
|
1170
1304
|
this: State['hasWhere'] extends true ? Collection<TContract, ModelName, Row, State> : never,
|
|
1305
|
+
configure?: (meta: MetaBuilder<'write'>) => void,
|
|
1306
|
+
): AsyncIterableResult<Row> {
|
|
1307
|
+
return (this as Collection<TContract, ModelName, Row, State>).#deleteAllWithAnnotations(
|
|
1308
|
+
this.#collectAnnotationsFromMeta(configure, 'write', 'deleteAll'),
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
#deleteAllWithAnnotations(
|
|
1313
|
+
annotationsMap: ReadonlyMap<string, AnnotationValue<unknown, OperationKind>> | undefined,
|
|
1171
1314
|
): AsyncIterableResult<Row> {
|
|
1172
1315
|
assertReturningCapability(this.contract, 'deleteAll()');
|
|
1173
|
-
return this.#executeDeleteReturning();
|
|
1316
|
+
return this.#executeDeleteReturning(annotationsMap);
|
|
1174
1317
|
}
|
|
1175
1318
|
|
|
1176
|
-
#executeDeleteReturning(
|
|
1319
|
+
#executeDeleteReturning(
|
|
1320
|
+
annotationsMap: ReadonlyMap<string, AnnotationValue<unknown, OperationKind>> | undefined,
|
|
1321
|
+
): AsyncIterableResult<Row> {
|
|
1177
1322
|
const parentJoinColumns = this.state.includes.map((include) => include.localColumn);
|
|
1178
1323
|
const { selectedForQuery: selectedForDelete, hiddenColumns } = augmentSelectionForJoinColumns(
|
|
1179
1324
|
this.state.selectedFields,
|
|
1180
1325
|
parentJoinColumns,
|
|
1181
1326
|
);
|
|
1182
|
-
const compiled =
|
|
1183
|
-
this.contract,
|
|
1184
|
-
|
|
1185
|
-
this.state.filters,
|
|
1186
|
-
selectedForDelete,
|
|
1327
|
+
const compiled = mergeAnnotations(
|
|
1328
|
+
compileDeleteReturning(this.contract, this.tableName, this.state.filters, selectedForDelete),
|
|
1329
|
+
annotationsMap,
|
|
1187
1330
|
);
|
|
1188
1331
|
return dispatchMutationRows<Row>({
|
|
1189
1332
|
contract: this.contract,
|
|
@@ -1198,7 +1341,11 @@ export class Collection<
|
|
|
1198
1341
|
|
|
1199
1342
|
async deleteCount(
|
|
1200
1343
|
this: State['hasWhere'] extends true ? Collection<TContract, ModelName, Row, State> : never,
|
|
1344
|
+
configure?: (meta: MetaBuilder<'write'>) => void,
|
|
1201
1345
|
): Promise<number> {
|
|
1346
|
+
// Annotations attach to the write, not the matching read.
|
|
1347
|
+
const annotationsMap = this.#collectAnnotationsFromMeta(configure, 'write', 'deleteCount');
|
|
1348
|
+
|
|
1202
1349
|
const primaryKeyColumn = resolvePrimaryKeyColumn(this.contract, this.tableName);
|
|
1203
1350
|
const countState: CollectionState = {
|
|
1204
1351
|
...emptyState(),
|
|
@@ -1211,7 +1358,10 @@ export class Collection<
|
|
|
1211
1358
|
countCompiled,
|
|
1212
1359
|
).toArray();
|
|
1213
1360
|
|
|
1214
|
-
const compiled =
|
|
1361
|
+
const compiled = mergeAnnotations(
|
|
1362
|
+
compileDeleteCount(this.contract, this.tableName, this.state.filters),
|
|
1363
|
+
annotationsMap,
|
|
1364
|
+
);
|
|
1215
1365
|
await executeQueryPlan<Record<string, unknown>>(this.ctx.runtime, compiled).toArray();
|
|
1216
1366
|
|
|
1217
1367
|
return matchingRows.length;
|
|
@@ -1388,4 +1538,60 @@ export class Collection<
|
|
|
1388
1538
|
modelName: this.modelName,
|
|
1389
1539
|
});
|
|
1390
1540
|
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* Invokes the user-supplied configurator (if any) against a freshly
|
|
1544
|
+
* constructed read meta builder, and returns a clone whose
|
|
1545
|
+
* `state.annotations` carries the recorded map. Used by read
|
|
1546
|
+
* terminals that flow annotations through state (`all`, `first`).
|
|
1547
|
+
*
|
|
1548
|
+
* Returns the receiver unchanged when no configurator was supplied
|
|
1549
|
+
* or when the configurator did not call `meta.annotate(...)`. The
|
|
1550
|
+
* meta builder's `annotate` method enforces applicability at the
|
|
1551
|
+
* type level and at runtime, so terminal code does not need to
|
|
1552
|
+
* re-validate.
|
|
1553
|
+
*/
|
|
1554
|
+
#withAnnotationsFromMeta(
|
|
1555
|
+
configure: ((meta: MetaBuilder<'read'>) => void) | undefined,
|
|
1556
|
+
terminalName: string,
|
|
1557
|
+
): this {
|
|
1558
|
+
if (configure === undefined) {
|
|
1559
|
+
return this;
|
|
1560
|
+
}
|
|
1561
|
+
const meta = createMetaBuilder('read', terminalName);
|
|
1562
|
+
configure(meta);
|
|
1563
|
+
if (meta.annotations.size === 0) {
|
|
1564
|
+
return this;
|
|
1565
|
+
}
|
|
1566
|
+
const next = new Map(this.state.annotations);
|
|
1567
|
+
for (const [namespace, value] of meta.annotations) {
|
|
1568
|
+
next.set(namespace, value);
|
|
1569
|
+
}
|
|
1570
|
+
return this.#clone({ annotations: next }) as this;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/**
|
|
1574
|
+
* Invokes the user-supplied configurator (if any) against a freshly
|
|
1575
|
+
* constructed meta builder of the given operation kind, and returns
|
|
1576
|
+
* the recorded annotation map (or `undefined` when empty). Used by
|
|
1577
|
+
* terminals where annotations don't flow through `state` — the
|
|
1578
|
+
* compiled plan is post-wrapped via `mergeAnnotations` instead.
|
|
1579
|
+
* Read terminals `all` and `first` populate `state.annotations`
|
|
1580
|
+
* via `#withAnnotationsFromMeta` instead; `aggregate` uses this
|
|
1581
|
+
* post-wrap path because its compile function doesn't take `state`.
|
|
1582
|
+
* The meta builder's `annotate` method enforces applicability at the
|
|
1583
|
+
* type level and at runtime.
|
|
1584
|
+
*/
|
|
1585
|
+
#collectAnnotationsFromMeta<K extends OperationKind>(
|
|
1586
|
+
configure: ((meta: MetaBuilder<K>) => void) | undefined,
|
|
1587
|
+
kind: K,
|
|
1588
|
+
terminalName: string,
|
|
1589
|
+
): ReadonlyMap<string, AnnotationValue<unknown, OperationKind>> | undefined {
|
|
1590
|
+
if (configure === undefined) {
|
|
1591
|
+
return undefined;
|
|
1592
|
+
}
|
|
1593
|
+
const meta = createMetaBuilder(kind, terminalName);
|
|
1594
|
+
configure(meta);
|
|
1595
|
+
return meta.annotations.size === 0 ? undefined : meta.annotations;
|
|
1596
|
+
}
|
|
1391
1597
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
import type {
|
|
3
|
+
AnnotationValue,
|
|
4
|
+
MetaBuilder,
|
|
5
|
+
OperationKind,
|
|
6
|
+
} from '@prisma-next/framework-components/runtime';
|
|
7
|
+
import { createMetaBuilder } from '@prisma-next/framework-components/runtime';
|
|
2
8
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
3
9
|
import {
|
|
4
10
|
AggregateExpr,
|
|
@@ -13,7 +19,7 @@ import { createAggregateBuilder, isAggregateSelector } from './aggregate-builder
|
|
|
13
19
|
import { getFieldToColumnMap } from './collection-contract';
|
|
14
20
|
import { mapStorageRowToModelFields } from './collection-runtime';
|
|
15
21
|
import { executeQueryPlan } from './execute-query-plan';
|
|
16
|
-
import { compileGroupedAggregate } from './query-plan';
|
|
22
|
+
import { compileGroupedAggregate, mergeAnnotations } from './query-plan';
|
|
17
23
|
import type {
|
|
18
24
|
AggregateBuilder,
|
|
19
25
|
AggregateResult,
|
|
@@ -82,8 +88,16 @@ export class GroupedCollection<
|
|
|
82
88
|
}) as GroupedCollection<TContract, ModelName, GroupFields>;
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Read terminal: run a grouped aggregate query.
|
|
93
|
+
*
|
|
94
|
+
* Accepts an optional `configure` callback that receives a
|
|
95
|
+
* `MetaBuilder<'read'>` for attaching typed annotations.
|
|
96
|
+
* Annotations are merged into the compiled plan's `meta.annotations`.
|
|
97
|
+
*/
|
|
85
98
|
async aggregate<Spec extends AggregateSpec>(
|
|
86
99
|
fn: (aggregate: AggregateBuilder<TContract, ModelName>) => Spec,
|
|
100
|
+
configure?: (meta: MetaBuilder<'read'>) => void,
|
|
87
101
|
): Promise<
|
|
88
102
|
Array<
|
|
89
103
|
SimplifyDeep<
|
|
@@ -103,13 +117,25 @@ export class GroupedCollection<
|
|
|
103
117
|
}
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
let annotationsMap: ReadonlyMap<string, AnnotationValue<unknown, OperationKind>> | undefined;
|
|
121
|
+
if (configure !== undefined) {
|
|
122
|
+
const meta = createMetaBuilder('read', 'groupBy.aggregate');
|
|
123
|
+
configure(meta);
|
|
124
|
+
if (meta.annotations.size > 0) {
|
|
125
|
+
annotationsMap = meta.annotations;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const compiled = mergeAnnotations(
|
|
130
|
+
compileGroupedAggregate(
|
|
131
|
+
this.contract,
|
|
132
|
+
this.tableName,
|
|
133
|
+
this.baseFilters,
|
|
134
|
+
this.groupByColumns,
|
|
135
|
+
aggregateSpec,
|
|
136
|
+
combineWhereExprs(this.havingFilters),
|
|
137
|
+
),
|
|
138
|
+
annotationsMap,
|
|
113
139
|
);
|
|
114
140
|
const rows = await executeQueryPlan<Record<string, unknown>>(
|
|
115
141
|
this.ctx.runtime,
|