@opensaas/stack-core 0.39.2 → 0.40.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +150 -0
- package/CLAUDE.md +51 -0
- package/dist/access/access-filter.d.ts +83 -10
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +139 -18
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.js +224 -12
- package/dist/access/access-filter.test.js.map +1 -1
- package/dist/access/engine.d.ts +43 -1
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +58 -0
- package/dist/access/engine.js.map +1 -1
- package/dist/access/engine.test.d.ts +2 -0
- package/dist/access/engine.test.d.ts.map +1 -0
- package/dist/access/engine.test.js +56 -0
- package/dist/access/engine.test.js.map +1 -0
- package/dist/access/errors.d.ts +28 -0
- package/dist/access/errors.d.ts.map +1 -1
- package/dist/access/errors.js +38 -2
- package/dist/access/errors.js.map +1 -1
- package/dist/access/field-access.d.ts +12 -0
- package/dist/access/field-access.d.ts.map +1 -1
- package/dist/access/field-access.js +22 -2
- package/dist/access/field-access.js.map +1 -1
- package/dist/access/field-access.test.js +66 -0
- package/dist/access/field-access.test.js.map +1 -1
- package/dist/access/field-visibility.d.ts +2 -1
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +45 -3
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +5 -2
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +4 -2
- package/dist/access/index.js.map +1 -1
- package/dist/config/types.d.ts +76 -10
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts +19 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +61 -17
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +54 -13
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/write-pipeline.d.ts +3 -2
- package/dist/context/write-pipeline.d.ts.map +1 -1
- package/dist/context/write-pipeline.js +8 -5
- package/dist/context/write-pipeline.js.map +1 -1
- package/dist/fields/index.d.ts +13 -1
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +50 -5
- package/dist/fields/index.js.map +1 -1
- package/dist/fields/relationship-foreign-key.test.d.ts +2 -0
- package/dist/fields/relationship-foreign-key.test.d.ts.map +1 -0
- package/dist/fields/relationship-foreign-key.test.js +18 -0
- package/dist/fields/relationship-foreign-key.test.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/case-utils.d.ts +15 -0
- package/dist/lib/case-utils.d.ts.map +1 -1
- package/dist/lib/case-utils.js +21 -0
- package/dist/lib/case-utils.js.map +1 -1
- package/dist/lib/case-utils.test.js +23 -1
- package/dist/lib/case-utils.test.js.map +1 -1
- package/dist/lib/prisma-errors.d.ts +38 -0
- package/dist/lib/prisma-errors.d.ts.map +1 -0
- package/dist/lib/prisma-errors.js +53 -0
- package/dist/lib/prisma-errors.js.map +1 -0
- package/dist/mcp/constants.d.ts +23 -0
- package/dist/mcp/constants.d.ts.map +1 -0
- package/dist/mcp/constants.js +23 -0
- package/dist/mcp/constants.js.map +1 -0
- package/dist/mcp/field-schema.d.ts +8 -0
- package/dist/mcp/field-schema.d.ts.map +1 -0
- package/dist/mcp/field-schema.js +69 -0
- package/dist/mcp/field-schema.js.map +1 -0
- package/dist/mcp/handler.d.ts.map +1 -1
- package/dist/mcp/handler.js +76 -90
- package/dist/mcp/handler.js.map +1 -1
- package/dist/mcp/projection.d.ts +62 -0
- package/dist/mcp/projection.d.ts.map +1 -0
- package/dist/mcp/projection.js +390 -0
- package/dist/mcp/projection.js.map +1 -0
- package/dist/utils/password.d.ts +3 -1
- package/dist/utils/password.d.ts.map +1 -1
- package/dist/utils/password.js +4 -2
- package/dist/utils/password.js.map +1 -1
- package/package.json +1 -1
- package/src/access/access-filter.test.ts +278 -12
- package/src/access/access-filter.ts +207 -19
- package/src/access/engine.test.ts +67 -0
- package/src/access/engine.ts +81 -1
- package/src/access/errors.ts +42 -2
- package/src/access/field-access.test.ts +78 -0
- package/src/access/field-access.ts +34 -2
- package/src/access/field-visibility.ts +75 -10
- package/src/access/index.ts +14 -0
- package/src/config/types.ts +76 -10
- package/src/context/index.ts +95 -15
- package/src/context/nested-operations.ts +61 -16
- package/src/context/write-pipeline.ts +8 -4
- package/src/fields/index.ts +55 -5
- package/src/fields/relationship-foreign-key.test.ts +29 -0
- package/src/index.ts +17 -1
- package/src/lib/case-utils.test.ts +32 -0
- package/src/lib/case-utils.ts +26 -0
- package/src/lib/prisma-errors.ts +86 -0
- package/src/mcp/constants.ts +24 -0
- package/src/mcp/field-schema.ts +84 -0
- package/src/mcp/handler.ts +109 -107
- package/src/mcp/projection.ts +497 -0
- package/src/utils/password.ts +5 -3
- package/tests/access-relationships.test.ts +3 -3
- package/tests/access.test.ts +83 -0
- package/tests/context.test.ts +259 -12
- package/tests/field-types.test.ts +113 -0
- package/tests/mcp-fields-projection-access.test.ts +250 -0
- package/tests/mcp-handler.test.ts +695 -4
- package/tests/nested-access-and-hooks.test.ts +65 -0
- package/tests/nested-write-synthetic-relation.test.ts +437 -0
- package/tests/password-types.test.ts +28 -0
- package/tests/password.test.ts +16 -2
- package/tests/prisma-errors.test.ts +141 -0
- package/tests/singleton.test.ts +9 -1
- package/tests/to-one-include-access-filter.test.ts +172 -0
- package/tests/with-session.test.ts +243 -0
- package/tests/write-pipeline.test.ts +29 -0
- package/tsconfig.tsbuildinfo +1 -1
package/tests/context.test.ts
CHANGED
|
@@ -895,6 +895,224 @@ describe('getContext', () => {
|
|
|
895
895
|
const created = result as { created: boolean; fieldErrors?: Record<string, string> }
|
|
896
896
|
expect(created.fieldErrors?.title).toBeDefined()
|
|
897
897
|
})
|
|
898
|
+
|
|
899
|
+
// Prisma 7 driver adapters (verified against @prisma/adapter-pg and PGlite,
|
|
900
|
+
// issue #979) leave `meta.target` undefined and put the equivalent data at
|
|
901
|
+
// `meta.driverAdapterError.cause` instead, as free-text-adjacent structured
|
|
902
|
+
// data rather than the documented shape.
|
|
903
|
+
describe('P2002 under Prisma 7 driver adapters (issue #979)', () => {
|
|
904
|
+
it('produces per-field errors for a composite unique violation', async () => {
|
|
905
|
+
mockPrisma.post.create.mockRejectedValue({
|
|
906
|
+
code: 'P2002',
|
|
907
|
+
meta: {
|
|
908
|
+
modelName: 'Post',
|
|
909
|
+
driverAdapterError: {
|
|
910
|
+
name: 'DriverAdapterError',
|
|
911
|
+
cause: {
|
|
912
|
+
originalCode: '23505',
|
|
913
|
+
originalMessage:
|
|
914
|
+
'duplicate key value violates unique constraint "post_title_content_key"',
|
|
915
|
+
kind: 'UniqueConstraintViolation',
|
|
916
|
+
constraint: { fields: ['title', 'content'] },
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
})
|
|
921
|
+
mockPrisma.user.findUnique.mockResolvedValue({ id: 'u1' })
|
|
922
|
+
|
|
923
|
+
const context = await getContext(config, mockPrisma, { userId: 'u1' })
|
|
924
|
+
const result = await context.serverAction({
|
|
925
|
+
listKey: 'Post',
|
|
926
|
+
action: 'createRelated',
|
|
927
|
+
data: { title: 'Dup', content: 'Dup' },
|
|
928
|
+
field: 'author',
|
|
929
|
+
parentId: 'u1',
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
expect(result).toMatchObject({ created: false })
|
|
933
|
+
const created = result as { created: boolean; fieldErrors?: Record<string, string> }
|
|
934
|
+
expect(created.fieldErrors?.title).toBeDefined()
|
|
935
|
+
expect(created.fieldErrors?.content).toBeDefined()
|
|
936
|
+
})
|
|
937
|
+
|
|
938
|
+
it('produces a field error for a single-column unique violation', async () => {
|
|
939
|
+
mockPrisma.post.create.mockRejectedValue({
|
|
940
|
+
code: 'P2002',
|
|
941
|
+
meta: {
|
|
942
|
+
modelName: 'Post',
|
|
943
|
+
driverAdapterError: {
|
|
944
|
+
cause: {
|
|
945
|
+
originalMessage:
|
|
946
|
+
'duplicate key value violates unique constraint "post_title_key"',
|
|
947
|
+
constraint: { fields: ['title'] },
|
|
948
|
+
},
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
})
|
|
952
|
+
mockPrisma.user.findUnique.mockResolvedValue({ id: 'u1' })
|
|
953
|
+
|
|
954
|
+
const context = await getContext(config, mockPrisma, { userId: 'u1' })
|
|
955
|
+
const result = await context.serverAction({
|
|
956
|
+
listKey: 'Post',
|
|
957
|
+
action: 'createRelated',
|
|
958
|
+
data: { title: 'Dup' },
|
|
959
|
+
field: 'author',
|
|
960
|
+
parentId: 'u1',
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
const created = result as { created: boolean; fieldErrors?: Record<string, string> }
|
|
964
|
+
expect(created.fieldErrors?.title).toBeDefined()
|
|
965
|
+
})
|
|
966
|
+
|
|
967
|
+
it('strips quotes so a camelCase column is keyed correctly (not left quoted)', async () => {
|
|
968
|
+
// Postgres quotes an identifier in `constraint.fields` only when it
|
|
969
|
+
// needed quoting — a camelCase column arrives as `"authorId"`. A naive
|
|
970
|
+
// fix that skips stripping would key fieldErrors by the literal string
|
|
971
|
+
// `"authorId"` (quotes included), missing the real field name.
|
|
972
|
+
mockPrisma.post.create.mockRejectedValue({
|
|
973
|
+
code: 'P2002',
|
|
974
|
+
meta: {
|
|
975
|
+
driverAdapterError: {
|
|
976
|
+
cause: {
|
|
977
|
+
originalMessage:
|
|
978
|
+
'duplicate key value violates unique constraint "post_authorId_key"',
|
|
979
|
+
constraint: { fields: ['"authorId"'] },
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
})
|
|
984
|
+
mockPrisma.user.findUnique.mockResolvedValue({ id: 'u1' })
|
|
985
|
+
|
|
986
|
+
const context = await getContext(config, mockPrisma, { userId: 'u1' })
|
|
987
|
+
const result = await context.serverAction({
|
|
988
|
+
listKey: 'Post',
|
|
989
|
+
action: 'createRelated',
|
|
990
|
+
data: { title: 'Dup' },
|
|
991
|
+
field: 'author',
|
|
992
|
+
parentId: 'u1',
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
const created = result as { created: boolean; fieldErrors?: Record<string, string> }
|
|
996
|
+
expect(created.fieldErrors).toEqual({ authorId: 'This value is already in use' })
|
|
997
|
+
})
|
|
998
|
+
|
|
999
|
+
it('humanizes a camelCase field name in the message instead of running words together', async () => {
|
|
1000
|
+
// Before this fix, `target` was always empty under a driver adapter, so
|
|
1001
|
+
// this label-formatting path never ran for a camelCase column. Recovering
|
|
1002
|
+
// real column names makes it reachable — verify it reads as words, not
|
|
1003
|
+
// "tenantid".
|
|
1004
|
+
const camelCaseConfig: OpenSaasConfig = {
|
|
1005
|
+
...config,
|
|
1006
|
+
lists: {
|
|
1007
|
+
...config.lists,
|
|
1008
|
+
Post: {
|
|
1009
|
+
...config.lists.Post,
|
|
1010
|
+
fields: { ...config.lists.Post.fields, tenantSlug: { type: 'text' } },
|
|
1011
|
+
},
|
|
1012
|
+
},
|
|
1013
|
+
}
|
|
1014
|
+
mockPrisma.post.create.mockRejectedValue({
|
|
1015
|
+
code: 'P2002',
|
|
1016
|
+
meta: {
|
|
1017
|
+
driverAdapterError: {
|
|
1018
|
+
cause: {
|
|
1019
|
+
originalMessage:
|
|
1020
|
+
'duplicate key value violates unique constraint "post_tenantSlug_key"',
|
|
1021
|
+
constraint: { fields: ['"tenantSlug"'] },
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
},
|
|
1025
|
+
})
|
|
1026
|
+
mockPrisma.user.findUnique.mockResolvedValue({ id: 'u1' })
|
|
1027
|
+
|
|
1028
|
+
const context = await getContext(camelCaseConfig, mockPrisma, { userId: 'u1' })
|
|
1029
|
+
const result = await context.serverAction({
|
|
1030
|
+
listKey: 'Post',
|
|
1031
|
+
action: 'createRelated',
|
|
1032
|
+
data: { title: 'Dup' },
|
|
1033
|
+
field: 'author',
|
|
1034
|
+
parentId: 'u1',
|
|
1035
|
+
})
|
|
1036
|
+
|
|
1037
|
+
const created = result as { created: boolean; fieldErrors?: Record<string, string> }
|
|
1038
|
+
expect(created.fieldErrors).toEqual({
|
|
1039
|
+
tenantSlug: 'This tenant slug is already in use',
|
|
1040
|
+
})
|
|
1041
|
+
})
|
|
1042
|
+
|
|
1043
|
+
it('leaves an already-populated meta.target unaffected (existing path still wins)', async () => {
|
|
1044
|
+
mockPrisma.post.create.mockRejectedValue({
|
|
1045
|
+
code: 'P2002',
|
|
1046
|
+
meta: {
|
|
1047
|
+
target: ['title'],
|
|
1048
|
+
// Present but must be ignored — meta.target already answers the question.
|
|
1049
|
+
driverAdapterError: {
|
|
1050
|
+
cause: {
|
|
1051
|
+
originalMessage: 'duplicate key value violates unique constraint "other_key"',
|
|
1052
|
+
constraint: { fields: ['content'] },
|
|
1053
|
+
},
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1056
|
+
})
|
|
1057
|
+
mockPrisma.user.findUnique.mockResolvedValue({ id: 'u1' })
|
|
1058
|
+
|
|
1059
|
+
const context = await getContext(config, mockPrisma, { userId: 'u1' })
|
|
1060
|
+
const result = await context.serverAction({
|
|
1061
|
+
listKey: 'Post',
|
|
1062
|
+
action: 'createRelated',
|
|
1063
|
+
data: { title: 'Dup' },
|
|
1064
|
+
field: 'author',
|
|
1065
|
+
parentId: 'u1',
|
|
1066
|
+
})
|
|
1067
|
+
|
|
1068
|
+
const created = result as { created: boolean; fieldErrors?: Record<string, string> }
|
|
1069
|
+
expect(created.fieldErrors).toEqual({ title: 'This title is already in use' })
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
it('falls back to the generic message (not a throw) when nothing is recoverable', async () => {
|
|
1073
|
+
mockPrisma.post.create.mockRejectedValue({ code: 'P2002', meta: {} })
|
|
1074
|
+
mockPrisma.user.findUnique.mockResolvedValue({ id: 'u1' })
|
|
1075
|
+
|
|
1076
|
+
const context = await getContext(config, mockPrisma, { userId: 'u1' })
|
|
1077
|
+
const result = await context.serverAction({
|
|
1078
|
+
listKey: 'Post',
|
|
1079
|
+
action: 'createRelated',
|
|
1080
|
+
data: { title: 'Dup' },
|
|
1081
|
+
field: 'author',
|
|
1082
|
+
parentId: 'u1',
|
|
1083
|
+
})
|
|
1084
|
+
|
|
1085
|
+
expect(result).toEqual({
|
|
1086
|
+
created: false,
|
|
1087
|
+
error: 'A record with this value already exists',
|
|
1088
|
+
fieldErrors: {},
|
|
1089
|
+
})
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
it('leaves a non-P2002 Prisma error unaffected', async () => {
|
|
1093
|
+
mockPrisma.post.create.mockRejectedValue({
|
|
1094
|
+
code: 'P2025',
|
|
1095
|
+
meta: { driverAdapterError: { cause: { constraint: { fields: ['title'] } } } },
|
|
1096
|
+
message: 'Record to update not found',
|
|
1097
|
+
})
|
|
1098
|
+
mockPrisma.user.findUnique.mockResolvedValue({ id: 'u1' })
|
|
1099
|
+
|
|
1100
|
+
const context = await getContext(config, mockPrisma, { userId: 'u1' })
|
|
1101
|
+
const result = await context.serverAction({
|
|
1102
|
+
listKey: 'Post',
|
|
1103
|
+
action: 'createRelated',
|
|
1104
|
+
data: { title: 'Dup' },
|
|
1105
|
+
field: 'author',
|
|
1106
|
+
parentId: 'u1',
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
expect(result).toEqual({
|
|
1110
|
+
created: false,
|
|
1111
|
+
error: 'Record to update not found',
|
|
1112
|
+
fieldErrors: {},
|
|
1113
|
+
})
|
|
1114
|
+
})
|
|
1115
|
+
})
|
|
898
1116
|
})
|
|
899
1117
|
|
|
900
1118
|
describe('relationshipOptions', () => {
|
|
@@ -2194,7 +2412,7 @@ describe('getContext', () => {
|
|
|
2194
2412
|
}
|
|
2195
2413
|
}
|
|
2196
2414
|
|
|
2197
|
-
it('findUnique inside a resolveOutput hook
|
|
2415
|
+
it('findUnique inside a resolveOutput hook still fetches the relation, unscoped at the Prisma level (#974)', async () => {
|
|
2198
2416
|
let innerIncludeSeen: unknown
|
|
2199
2417
|
const hookConfig = configWithResolveOutputProbe({ post: true }, (include) => {
|
|
2200
2418
|
innerIncludeSeen = include
|
|
@@ -2206,9 +2424,11 @@ describe('getContext', () => {
|
|
|
2206
2424
|
const context = await getContext(hookConfig, relPrisma, null)
|
|
2207
2425
|
await context.db.author.findUnique({ where: { id: 'a1' } })
|
|
2208
2426
|
|
|
2209
|
-
// `post` is still fetched (not dropped)
|
|
2210
|
-
//
|
|
2211
|
-
|
|
2427
|
+
// `post` is still fetched (not dropped). It carries no `where` — Post
|
|
2428
|
+
// is a to-one relation here, and Prisma rejects a `where` on a to-one
|
|
2429
|
+
// include (#974); Post's own access filter is enforced afterward via
|
|
2430
|
+
// a batched existence check instead (see access-filter.test.ts).
|
|
2431
|
+
expect(innerIncludeSeen).toEqual({ post: true })
|
|
2212
2432
|
})
|
|
2213
2433
|
|
|
2214
2434
|
it('findMany inside a resolveOutput hook scopes the relation but does not auto-expand its nested include', async () => {
|
|
@@ -2227,10 +2447,10 @@ describe('getContext', () => {
|
|
|
2227
2447
|
await context.db.author.findMany()
|
|
2228
2448
|
|
|
2229
2449
|
// The caller's own nested selection (`post.include.author`) is honoured
|
|
2230
|
-
// as-is (access control does not auto-descend further here)
|
|
2231
|
-
// `
|
|
2450
|
+
// as-is (access control does not auto-descend further here). `post`
|
|
2451
|
+
// itself carries no `where` — see the findUnique test above (#974).
|
|
2232
2452
|
expect(innerIncludeSeen).toEqual({
|
|
2233
|
-
post: {
|
|
2453
|
+
post: { include: { author: true } },
|
|
2234
2454
|
})
|
|
2235
2455
|
})
|
|
2236
2456
|
})
|
|
@@ -2289,7 +2509,7 @@ describe('getContext', () => {
|
|
|
2289
2509
|
expect(chainPrisma.c0.findMany).not.toHaveBeenCalled()
|
|
2290
2510
|
})
|
|
2291
2511
|
|
|
2292
|
-
it('still
|
|
2512
|
+
it('still applies row-scoping at the deepest hop for the same include one hop shallower', async () => {
|
|
2293
2513
|
const chainLength = READ_INCLUDE_MAX_DEPTH + 1
|
|
2294
2514
|
const chainConfig: OpenSaasConfig = {
|
|
2295
2515
|
db: { provider: 'postgresql', url: 'postgresql://localhost:5432/test' },
|
|
@@ -2300,7 +2520,20 @@ describe('getContext', () => {
|
|
|
2300
2520
|
for (let i = 0; i < chainLength; i++) {
|
|
2301
2521
|
chainPrisma[`c${i}`] = { findMany: vi.fn(), findFirst: vi.fn(), findUnique: vi.fn() }
|
|
2302
2522
|
}
|
|
2303
|
-
|
|
2523
|
+
|
|
2524
|
+
// Every hop here is to-one (no `many: true`), so none of them can
|
|
2525
|
+
// carry a `where` on the Prisma include (#974) — the chain comes
|
|
2526
|
+
// back unscoped at the Prisma level, one nested object per hop, and
|
|
2527
|
+
// row-scoping is enforced afterward via a batched existence check
|
|
2528
|
+
// per hop instead.
|
|
2529
|
+
let deepestItem: Record<string, unknown> = { id: `id${chainLength - 1}`, name: 'leaf' }
|
|
2530
|
+
for (let i = chainLength - 2; i >= 0; i--) {
|
|
2531
|
+
deepestItem = { id: `id${i}`, name: 'x', next: deepestItem }
|
|
2532
|
+
}
|
|
2533
|
+
chainPrisma.c0.findMany.mockResolvedValue([deepestItem])
|
|
2534
|
+
for (let i = 1; i < chainLength; i++) {
|
|
2535
|
+
chainPrisma[`c${i}`].findMany.mockResolvedValue([{ id: `id${i}` }])
|
|
2536
|
+
}
|
|
2304
2537
|
|
|
2305
2538
|
const context = await getContext(chainConfig, chainPrisma, null)
|
|
2306
2539
|
|
|
@@ -2308,13 +2541,27 @@ describe('getContext', () => {
|
|
|
2308
2541
|
include: { next: nestedCallerInclude(READ_INCLUDE_MAX_DEPTH - 1) },
|
|
2309
2542
|
})
|
|
2310
2543
|
|
|
2311
|
-
// Walk the built include down to the last list —
|
|
2312
|
-
//
|
|
2544
|
+
// Walk the built include down to the last list — no hop carries a
|
|
2545
|
+
// `where` (they are all to-one).
|
|
2313
2546
|
let entry = chainPrisma.c0.findMany.mock.calls[0][0].include.next
|
|
2314
2547
|
for (let i = 1; i < READ_INCLUDE_MAX_DEPTH; i++) {
|
|
2548
|
+
expect(entry.where).toBeUndefined()
|
|
2315
2549
|
entry = entry.include.next
|
|
2316
2550
|
}
|
|
2317
|
-
expect(entry.where).
|
|
2551
|
+
expect(entry.where).toBeUndefined()
|
|
2552
|
+
|
|
2553
|
+
// The deepest hop's own existence check still carries that list's
|
|
2554
|
+
// access filter — row scoping reaches all the way down, just not
|
|
2555
|
+
// through the Prisma `include` itself.
|
|
2556
|
+
expect(chainPrisma[`c${READ_INCLUDE_MAX_DEPTH}`].findMany).toHaveBeenCalledWith({
|
|
2557
|
+
where: {
|
|
2558
|
+
AND: [
|
|
2559
|
+
{ ownerId: { equals: `C${READ_INCLUDE_MAX_DEPTH}` } },
|
|
2560
|
+
{ id: { in: [`id${READ_INCLUDE_MAX_DEPTH}`] } },
|
|
2561
|
+
],
|
|
2562
|
+
},
|
|
2563
|
+
select: { id: true },
|
|
2564
|
+
})
|
|
2318
2565
|
})
|
|
2319
2566
|
})
|
|
2320
2567
|
|
|
@@ -307,6 +307,29 @@ describe('Field Types', () => {
|
|
|
307
307
|
expect(prismaType.type).toBe('Int')
|
|
308
308
|
expect(prismaType.modifiers).toBe('@db.BigInt')
|
|
309
309
|
})
|
|
310
|
+
|
|
311
|
+
test('isIndexed: true requests a block-level index', () => {
|
|
312
|
+
const field = integer({ isIndexed: true })
|
|
313
|
+
const prismaType = field.getPrismaType('rank')
|
|
314
|
+
|
|
315
|
+
expect(prismaType.index).toBe(true)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test("isIndexed: 'unique' generates @unique modifier", () => {
|
|
319
|
+
const field = integer({ isIndexed: 'unique' })
|
|
320
|
+
const prismaType = field.getPrismaType('rank')
|
|
321
|
+
|
|
322
|
+
expect(prismaType.modifiers).toContain('@unique')
|
|
323
|
+
expect(prismaType.index).toBeUndefined()
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('no isIndexed generates neither modifier nor index', () => {
|
|
327
|
+
const field = integer()
|
|
328
|
+
const prismaType = field.getPrismaType('rank')
|
|
329
|
+
|
|
330
|
+
expect(prismaType.modifiers).not.toContain('@unique')
|
|
331
|
+
expect(prismaType.index).toBeUndefined()
|
|
332
|
+
})
|
|
310
333
|
})
|
|
311
334
|
|
|
312
335
|
describe('getTypeScriptType', () => {
|
|
@@ -679,6 +702,29 @@ describe('Field Types', () => {
|
|
|
679
702
|
expect(prismaType.type).toBe('DateTime')
|
|
680
703
|
expect(prismaType.modifiers).toContain('@db.Timestamptz')
|
|
681
704
|
})
|
|
705
|
+
|
|
706
|
+
test('isIndexed: true requests a block-level index', () => {
|
|
707
|
+
const field = timestamp({ isIndexed: true })
|
|
708
|
+
const prismaType = field.getPrismaType('publishedAt')
|
|
709
|
+
|
|
710
|
+
expect(prismaType.index).toBe(true)
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
test("isIndexed: 'unique' generates @unique modifier", () => {
|
|
714
|
+
const field = timestamp({ isIndexed: 'unique' })
|
|
715
|
+
const prismaType = field.getPrismaType('publishedAt')
|
|
716
|
+
|
|
717
|
+
expect(prismaType.modifiers).toContain('@unique')
|
|
718
|
+
expect(prismaType.index).toBeUndefined()
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
test('no isIndexed generates neither modifier nor index', () => {
|
|
722
|
+
const field = timestamp()
|
|
723
|
+
const prismaType = field.getPrismaType('publishedAt')
|
|
724
|
+
|
|
725
|
+
expect(prismaType.modifiers).not.toContain('@unique')
|
|
726
|
+
expect(prismaType.index).toBeUndefined()
|
|
727
|
+
})
|
|
682
728
|
})
|
|
683
729
|
|
|
684
730
|
describe('getTypeScriptType', () => {
|
|
@@ -810,6 +856,27 @@ describe('Field Types', () => {
|
|
|
810
856
|
)
|
|
811
857
|
})
|
|
812
858
|
})
|
|
859
|
+
|
|
860
|
+
describe('ui.listView.defaultColumn (issue #1018)', () => {
|
|
861
|
+
test('is excluded from default admin table columns by default', () => {
|
|
862
|
+
const field = password()
|
|
863
|
+
|
|
864
|
+
expect(field.ui?.listView?.defaultColumn).toBe(false)
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
test('can be opted back into default columns explicitly', () => {
|
|
868
|
+
const field = password({ ui: { listView: { defaultColumn: true } } })
|
|
869
|
+
|
|
870
|
+
expect(field.ui?.listView?.defaultColumn).toBe(true)
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
test('preserves other caller-supplied ui options', () => {
|
|
874
|
+
const field = password({ ui: { description: 'Account password' } })
|
|
875
|
+
|
|
876
|
+
expect(field.ui?.description).toBe('Account password')
|
|
877
|
+
expect(field.ui?.listView?.defaultColumn).toBe(false)
|
|
878
|
+
})
|
|
879
|
+
})
|
|
813
880
|
})
|
|
814
881
|
|
|
815
882
|
describe('select field', () => {
|
|
@@ -905,6 +972,52 @@ describe('Field Types', () => {
|
|
|
905
972
|
expect(prismaType.type).toBe('String')
|
|
906
973
|
expect(prismaType.modifiers).toBe(' @default("draft")')
|
|
907
974
|
})
|
|
975
|
+
|
|
976
|
+
test('isIndexed: true requests a block-level index on the default string column', () => {
|
|
977
|
+
const field = select({
|
|
978
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
979
|
+
isIndexed: true,
|
|
980
|
+
})
|
|
981
|
+
const prismaType = field.getPrismaType('status')
|
|
982
|
+
|
|
983
|
+
expect(prismaType.type).toBe('String')
|
|
984
|
+
expect(prismaType.index).toBe(true)
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
test("isIndexed: 'unique' generates @unique on the default string column", () => {
|
|
988
|
+
const field = select({
|
|
989
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
990
|
+
isIndexed: 'unique',
|
|
991
|
+
})
|
|
992
|
+
const prismaType = field.getPrismaType('status')
|
|
993
|
+
|
|
994
|
+
expect(prismaType.modifiers).toContain('@unique')
|
|
995
|
+
expect(prismaType.index).toBeUndefined()
|
|
996
|
+
})
|
|
997
|
+
|
|
998
|
+
test('isIndexed: true requests a block-level index on a native-enum column', () => {
|
|
999
|
+
const field = select({
|
|
1000
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
1001
|
+
db: { type: 'enum' },
|
|
1002
|
+
isIndexed: true,
|
|
1003
|
+
})
|
|
1004
|
+
const prismaType = field.getPrismaType('status', undefined, 'Post')
|
|
1005
|
+
|
|
1006
|
+
expect(prismaType.type).toBe('PostStatus')
|
|
1007
|
+
expect(prismaType.index).toBe(true)
|
|
1008
|
+
})
|
|
1009
|
+
|
|
1010
|
+
test("isIndexed: 'unique' generates @unique on a native-enum column", () => {
|
|
1011
|
+
const field = select({
|
|
1012
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
1013
|
+
db: { type: 'enum' },
|
|
1014
|
+
isIndexed: 'unique',
|
|
1015
|
+
})
|
|
1016
|
+
const prismaType = field.getPrismaType('status', undefined, 'Post')
|
|
1017
|
+
|
|
1018
|
+
expect(prismaType.modifiers).toContain('@unique')
|
|
1019
|
+
expect(prismaType.index).toBeUndefined()
|
|
1020
|
+
})
|
|
908
1021
|
})
|
|
909
1022
|
|
|
910
1023
|
describe('getTypeScriptType', () => {
|