joist-codegen 2.3.0-next.12 → 2.3.0-next.121
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/build/EntityDbMetadata.cjs +672 -0
- package/build/EntityDbMetadata.cjs.map +1 -0
- package/build/EntityDbMetadata.d.cts +301 -0
- package/build/EntityDbMetadata.d.cts.map +1 -0
- package/build/EntityDbMetadata.d.mts +301 -0
- package/build/EntityDbMetadata.d.mts.map +1 -0
- package/build/EntityDbMetadata.js +550 -664
- package/build/EntityDbMetadata.js.map +1 -1
- package/build/_virtual/_rolldown/runtime.cjs +23 -0
- package/build/_virtual/_rolldown/runtime.js +5 -0
- package/build/assignTags.cjs +45 -0
- package/build/assignTags.cjs.map +1 -0
- package/build/{assignTags.d.ts → assignTags.d.cts} +8 -4
- package/build/assignTags.d.cts.map +1 -0
- package/build/assignTags.d.mts +16 -0
- package/build/assignTags.d.mts.map +1 -0
- package/build/assignTags.js +37 -59
- package/build/assignTags.js.map +1 -1
- package/build/codemods/Codemod.cjs +2 -0
- package/build/codemods/Codemod.d.cts +17 -0
- package/build/codemods/Codemod.d.cts.map +1 -0
- package/build/codemods/Codemod.d.mts +17 -0
- package/build/codemods/Codemod.d.mts.map +1 -0
- package/build/codemods/Codemod.js +2 -3
- package/build/codemods/JscodeshiftMod.cjs +42 -0
- package/build/codemods/JscodeshiftMod.cjs.map +1 -0
- package/build/codemods/JscodeshiftMod.d.cts +14 -0
- package/build/codemods/JscodeshiftMod.d.cts.map +1 -0
- package/build/codemods/JscodeshiftMod.d.mts +14 -0
- package/build/codemods/JscodeshiftMod.d.mts.map +1 -0
- package/build/codemods/JscodeshiftMod.js +33 -33
- package/build/codemods/JscodeshiftMod.js.map +1 -1
- package/build/codemods/codemod_0001-rename_has_async_property.cjs +30 -0
- package/build/codemods/codemod_0001-rename_has_async_property.cjs.map +1 -0
- package/build/codemods/codemod_0001-rename_has_async_property.d.cts +9 -0
- package/build/codemods/codemod_0001-rename_has_async_property.d.cts.map +1 -0
- package/build/codemods/codemod_0001-rename_has_async_property.d.mts +9 -0
- package/build/codemods/codemod_0001-rename_has_async_property.d.mts.map +1 -0
- package/build/codemods/codemod_0001-rename_has_async_property.js +21 -25
- package/build/codemods/codemod_0001-rename_has_async_property.js.map +1 -1
- package/build/codemods/codemod_0002-rename_async_query_fields.cjs +50 -0
- package/build/codemods/codemod_0002-rename_async_query_fields.cjs.map +1 -0
- package/build/codemods/codemod_0002-rename_async_query_fields.d.cts +9 -0
- package/build/codemods/codemod_0002-rename_async_query_fields.d.cts.map +1 -0
- package/build/codemods/codemod_0002-rename_async_query_fields.d.mts +9 -0
- package/build/codemods/codemod_0002-rename_async_query_fields.d.mts.map +1 -0
- package/build/codemods/codemod_0002-rename_async_query_fields.js +38 -52
- package/build/codemods/codemod_0002-rename_async_query_fields.js.map +1 -1
- package/build/codemods/codemod_0003-rename_find_paginated.cjs +25 -0
- package/build/codemods/codemod_0003-rename_find_paginated.cjs.map +1 -0
- package/build/codemods/codemod_0003-rename_find_paginated.d.cts +9 -0
- package/build/codemods/codemod_0003-rename_find_paginated.d.cts.map +1 -0
- package/build/codemods/codemod_0003-rename_find_paginated.d.mts +9 -0
- package/build/codemods/codemod_0003-rename_find_paginated.d.mts.map +1 -0
- package/build/codemods/codemod_0003-rename_find_paginated.js +17 -26
- package/build/codemods/codemod_0003-rename_find_paginated.js.map +1 -1
- package/build/codemods/index.cjs +49 -0
- package/build/codemods/index.cjs.map +1 -0
- package/build/codemods/index.d.cts +12 -0
- package/build/codemods/index.d.cts.map +1 -0
- package/build/codemods/index.d.mts +12 -0
- package/build/codemods/index.d.mts.map +1 -0
- package/build/codemods/index.js +34 -42
- package/build/codemods/index.js.map +1 -1
- package/build/config.cjs +377 -0
- package/build/config.cjs.map +1 -0
- package/build/config.d.cts +181 -0
- package/build/config.d.cts.map +1 -0
- package/build/config.d.mts +181 -0
- package/build/config.d.mts.map +1 -0
- package/build/config.js +306 -356
- package/build/config.js.map +1 -1
- package/build/docs/cache.cjs +62 -0
- package/build/docs/cache.cjs.map +1 -0
- package/build/docs/cache.d.cts +17 -0
- package/build/docs/cache.d.cts.map +1 -0
- package/build/docs/cache.d.mts +17 -0
- package/build/docs/cache.d.mts.map +1 -0
- package/build/docs/cache.js +52 -53
- package/build/docs/cache.js.map +1 -1
- package/build/docs/generate-metadata-docs.cjs +32 -0
- package/build/docs/generate-metadata-docs.cjs.map +1 -0
- package/build/docs/generate-metadata-docs.d.cts +9 -0
- package/build/docs/generate-metadata-docs.d.cts.map +1 -0
- package/build/docs/generate-metadata-docs.d.mts +9 -0
- package/build/docs/generate-metadata-docs.d.mts.map +1 -0
- package/build/docs/generate-metadata-docs.js +19 -24
- package/build/docs/generate-metadata-docs.js.map +1 -1
- package/build/docs/index.cjs +90 -0
- package/build/docs/index.cjs.map +1 -0
- package/build/docs/index.d.cts +16 -0
- package/build/docs/index.d.cts.map +1 -0
- package/build/docs/index.d.mts +16 -0
- package/build/docs/index.d.mts.map +1 -0
- package/build/docs/index.js +75 -80
- package/build/docs/index.js.map +1 -1
- package/build/docs/markdown.cjs +181 -0
- package/build/docs/markdown.cjs.map +1 -0
- package/build/docs/{markdown.d.ts → markdown.d.cts} +14 -12
- package/build/docs/markdown.d.cts.map +1 -0
- package/build/docs/markdown.d.mts +32 -0
- package/build/docs/markdown.d.mts.map +1 -0
- package/build/docs/markdown.js +152 -220
- package/build/docs/markdown.js.map +1 -1
- package/build/docs/parsing.cjs +117 -0
- package/build/docs/parsing.cjs.map +1 -0
- package/build/docs/parsing.d.cts +48 -0
- package/build/docs/parsing.d.cts.map +1 -0
- package/build/docs/parsing.d.mts +48 -0
- package/build/docs/parsing.d.mts.map +1 -0
- package/build/docs/parsing.js +89 -88
- package/build/docs/parsing.js.map +1 -1
- package/build/findEntityScopes.cjs +119 -0
- package/build/findEntityScopes.cjs.map +1 -0
- package/build/findEntityScopes.d.cts +14 -0
- package/build/findEntityScopes.d.cts.map +1 -0
- package/build/findEntityScopes.d.mts +14 -0
- package/build/findEntityScopes.d.mts.map +1 -0
- package/build/findEntityScopes.js +83 -111
- package/build/findEntityScopes.js.map +1 -1
- package/build/foreignKeyOrdering.cjs +52 -0
- package/build/foreignKeyOrdering.cjs.map +1 -0
- package/build/foreignKeyOrdering.d.cts +8 -0
- package/build/foreignKeyOrdering.d.cts.map +1 -0
- package/build/foreignKeyOrdering.d.mts +8 -0
- package/build/foreignKeyOrdering.d.mts.map +1 -0
- package/build/foreignKeyOrdering.js +46 -55
- package/build/foreignKeyOrdering.js.map +1 -1
- package/build/generate.cjs +128 -0
- package/build/generate.cjs.map +1 -0
- package/build/generate.d.cts +17 -0
- package/build/generate.d.cts.map +1 -0
- package/build/generate.d.mts +17 -0
- package/build/generate.d.mts.map +1 -0
- package/build/generate.js +111 -124
- package/build/generate.js.map +1 -1
- package/build/generateEntitiesFile.cjs +46 -0
- package/build/generateEntitiesFile.cjs.map +1 -0
- package/build/generateEntitiesFile.d.cts +11 -0
- package/build/generateEntitiesFile.d.cts.map +1 -0
- package/build/generateEntitiesFile.d.mts +11 -0
- package/build/generateEntitiesFile.d.mts.map +1 -0
- package/build/generateEntitiesFile.js +28 -25
- package/build/generateEntitiesFile.js.map +1 -1
- package/build/generateEntityCodegenFile.cjs +1011 -0
- package/build/generateEntityCodegenFile.cjs.map +1 -0
- package/build/generateEntityCodegenFile.d.cts +16 -0
- package/build/generateEntityCodegenFile.d.cts.map +1 -0
- package/build/generateEntityCodegenFile.d.mts +16 -0
- package/build/generateEntityCodegenFile.d.mts.map +1 -0
- package/build/generateEntityCodegenFile.js +794 -827
- package/build/generateEntityCodegenFile.js.map +1 -1
- package/build/generateEntityFile.cjs +23 -0
- package/build/generateEntityFile.cjs.map +1 -0
- package/build/generateEntityFile.d.cts +9 -0
- package/build/generateEntityFile.d.cts.map +1 -0
- package/build/generateEntityFile.d.mts +9 -0
- package/build/generateEntityFile.d.mts.map +1 -0
- package/build/generateEntityFile.js +11 -8
- package/build/generateEntityFile.js.map +1 -1
- package/build/generateEntityTestFile.cjs +25 -0
- package/build/generateEntityTestFile.cjs.map +1 -0
- package/build/generateEntityTestFile.d.cts +9 -0
- package/build/generateEntityTestFile.d.cts.map +1 -0
- package/build/generateEntityTestFile.d.mts +9 -0
- package/build/generateEntityTestFile.d.mts.map +1 -0
- package/build/generateEntityTestFile.js +10 -7
- package/build/generateEntityTestFile.js.map +1 -1
- package/build/generateEnumFile.cjs +73 -0
- package/build/generateEnumFile.cjs.map +1 -0
- package/build/generateEnumFile.d.cts +9 -0
- package/build/generateEnumFile.d.cts.map +1 -0
- package/build/generateEnumFile.d.mts +9 -0
- package/build/generateEnumFile.d.mts.map +1 -0
- package/build/generateEnumFile.js +33 -43
- package/build/generateEnumFile.js.map +1 -1
- package/build/generateFactoriesFiles.cjs +28 -0
- package/build/generateFactoriesFiles.cjs.map +1 -0
- package/build/generateFactoriesFiles.d.cts +7 -0
- package/build/generateFactoriesFiles.d.cts.map +1 -0
- package/build/generateFactoriesFiles.d.mts +7 -0
- package/build/generateFactoriesFiles.d.mts.map +1 -0
- package/build/generateFactoriesFiles.js +21 -16
- package/build/generateFactoriesFiles.js.map +1 -1
- package/build/generateFlushFunction.cjs +61 -0
- package/build/generateFlushFunction.cjs.map +1 -0
- package/build/{generateFlushFunction.d.ts → generateFlushFunction.d.cts} +8 -5
- package/build/generateFlushFunction.d.cts.map +1 -0
- package/build/generateFlushFunction.d.mts +23 -0
- package/build/generateFlushFunction.d.mts.map +1 -0
- package/build/generateFlushFunction.js +32 -78
- package/build/generateFlushFunction.js.map +1 -1
- package/build/generateMetadataFile.cjs +315 -0
- package/build/generateMetadataFile.cjs.map +1 -0
- package/build/generateMetadataFile.d.cts +10 -0
- package/build/generateMetadataFile.d.cts.map +1 -0
- package/build/generateMetadataFile.d.mts +10 -0
- package/build/generateMetadataFile.d.mts.map +1 -0
- package/build/generateMetadataFile.js +151 -157
- package/build/generateMetadataFile.js.map +1 -1
- package/build/generatePgEnumFile.cjs +21 -0
- package/build/generatePgEnumFile.cjs.map +1 -0
- package/build/generatePgEnumFile.d.cts +9 -0
- package/build/generatePgEnumFile.d.cts.map +1 -0
- package/build/generatePgEnumFile.d.mts +9 -0
- package/build/generatePgEnumFile.d.mts.map +1 -0
- package/build/generatePgEnumFile.js +12 -9
- package/build/generatePgEnumFile.js.map +1 -1
- package/build/index.cjs +135 -0
- package/build/index.cjs.map +1 -0
- package/build/index.d.cts +12 -0
- package/build/index.d.cts.map +1 -0
- package/build/index.d.mts +12 -0
- package/build/index.d.mts.map +1 -0
- package/build/index.js +109 -127
- package/build/index.js.map +1 -1
- package/build/inheritance.cjs +258 -0
- package/build/inheritance.cjs.map +1 -0
- package/build/inheritance.d.cts +13 -0
- package/build/inheritance.d.cts.map +1 -0
- package/build/inheritance.d.mts +13 -0
- package/build/inheritance.d.mts.map +1 -0
- package/build/inheritance.js +238 -247
- package/build/inheritance.js.map +1 -1
- package/build/installSkills.cjs +68 -0
- package/build/installSkills.cjs.map +1 -0
- package/build/installSkills.d.cts +16 -0
- package/build/installSkills.d.cts.map +1 -0
- package/build/installSkills.d.mts +16 -0
- package/build/installSkills.d.mts.map +1 -0
- package/build/installSkills.js +67 -0
- package/build/installSkills.js.map +1 -0
- package/build/keywords.cjs +7 -0
- package/build/keywords.cjs.map +1 -0
- package/build/keywords.d.cts +5 -0
- package/build/keywords.d.cts.map +1 -0
- package/build/keywords.d.mts +5 -0
- package/build/keywords.d.mts.map +1 -0
- package/build/keywords.js +5 -4
- package/build/keywords.js.map +1 -1
- package/build/loadMetadata.cjs +39 -0
- package/build/loadMetadata.cjs.map +1 -0
- package/build/loadMetadata.d.cts +31 -0
- package/build/loadMetadata.d.cts.map +1 -0
- package/build/loadMetadata.d.mts +31 -0
- package/build/loadMetadata.d.mts.map +1 -0
- package/build/loadMetadata.js +32 -39
- package/build/loadMetadata.js.map +1 -1
- package/build/logger.cjs +28 -0
- package/build/logger.cjs.map +1 -0
- package/build/logger.d.cts +13 -0
- package/build/logger.d.cts.map +1 -0
- package/build/logger.d.mts +13 -0
- package/build/logger.d.mts.map +1 -0
- package/build/logger.js +17 -16
- package/build/logger.js.map +1 -1
- package/build/pgMetadata.cjs +332 -0
- package/build/pgMetadata.cjs.map +1 -0
- package/build/pgMetadata.d.cts +128 -0
- package/build/pgMetadata.d.cts.map +1 -0
- package/build/pgMetadata.d.mts +128 -0
- package/build/pgMetadata.d.mts.map +1 -0
- package/build/pgMetadata.js +320 -0
- package/build/pgMetadata.js.map +1 -0
- package/build/scanEntityFiles.cjs +46 -0
- package/build/scanEntityFiles.cjs.map +1 -0
- package/build/scanEntityFiles.d.cts +8 -0
- package/build/scanEntityFiles.d.cts.map +1 -0
- package/build/scanEntityFiles.d.mts +8 -0
- package/build/scanEntityFiles.d.mts.map +1 -0
- package/build/scanEntityFiles.js +37 -42
- package/build/scanEntityFiles.js.map +1 -1
- package/build/sortForeignKeys.cjs +99 -0
- package/build/sortForeignKeys.cjs.map +1 -0
- package/build/{sortForeignKeys.d.ts → sortForeignKeys.d.cts} +8 -5
- package/build/sortForeignKeys.d.cts.map +1 -0
- package/build/sortForeignKeys.d.mts +35 -0
- package/build/sortForeignKeys.d.mts.map +1 -0
- package/build/sortForeignKeys.js +88 -117
- package/build/sortForeignKeys.js.map +1 -1
- package/build/symbols.cjs +238 -0
- package/build/symbols.cjs.map +1 -0
- package/build/symbols.d.cts +120 -0
- package/build/symbols.d.cts.map +1 -0
- package/build/symbols.d.mts +120 -0
- package/build/symbols.d.mts.map +1 -0
- package/build/symbols.js +121 -113
- package/build/symbols.js.map +1 -1
- package/build/tsdoc.cjs +22 -0
- package/build/tsdoc.cjs.map +1 -0
- package/build/tsdoc.d.cts +15 -0
- package/build/tsdoc.d.cts.map +1 -0
- package/build/tsdoc.d.mts +15 -0
- package/build/tsdoc.d.mts.map +1 -0
- package/build/tsdoc.js +18 -15
- package/build/tsdoc.js.map +1 -1
- package/build/utils.cjs +175 -0
- package/build/utils.cjs.map +1 -0
- package/build/utils.d.cts +36 -0
- package/build/utils.d.cts.map +1 -0
- package/build/utils.d.mts +36 -0
- package/build/utils.d.mts.map +1 -0
- package/build/utils.js +113 -166
- package/build/utils.js.map +1 -1
- package/build/write-json-schema.cjs +23 -0
- package/build/write-json-schema.cjs.map +1 -0
- package/build/write-json-schema.d.cts +1 -0
- package/build/write-json-schema.d.mts +1 -0
- package/build/write-json-schema.js +19 -13
- package/build/write-json-schema.js.map +1 -1
- package/package.json +47 -18
- package/skills/joist-docs/SKILL.md +142 -0
- package/skills/joist-em-basics/SKILL.md +114 -0
- package/skills/joist-partial-updates/SKILL.md +352 -0
- package/skills/joist-reactive-hints/SKILL.md +150 -0
- package/skills/joist-test-factories/SKILL.md +397 -0
- package/skills/joist-upsert/SKILL.md +95 -0
- package/build/EntityDbMetadata.d.ts +0 -271
- package/build/EntityDbMetadata.d.ts.map +0 -1
- package/build/EntityDbMetadata.test.d.ts +0 -2
- package/build/EntityDbMetadata.test.d.ts.map +0 -1
- package/build/EntityDbMetadata.test.js +0 -211
- package/build/EntityDbMetadata.test.js.map +0 -1
- package/build/assignTags.d.ts.map +0 -1
- package/build/codemods/Codemod.d.ts +0 -14
- package/build/codemods/Codemod.d.ts.map +0 -1
- package/build/codemods/Codemod.js.map +0 -1
- package/build/codemods/JscodeshiftMod.d.ts +0 -11
- package/build/codemods/JscodeshiftMod.d.ts.map +0 -1
- package/build/codemods/__tests__/codemod_0001-rename_has_async_property.test.d.ts +0 -2
- package/build/codemods/__tests__/codemod_0001-rename_has_async_property.test.d.ts.map +0 -1
- package/build/codemods/__tests__/codemod_0001-rename_has_async_property.test.js +0 -9
- package/build/codemods/__tests__/codemod_0001-rename_has_async_property.test.js.map +0 -1
- package/build/codemods/__tests__/codemod_0002-rename_async_query_fields.test.d.ts +0 -2
- package/build/codemods/__tests__/codemod_0002-rename_async_query_fields.test.d.ts.map +0 -1
- package/build/codemods/__tests__/codemod_0002-rename_async_query_fields.test.js +0 -9
- package/build/codemods/__tests__/codemod_0002-rename_async_query_fields.test.js.map +0 -1
- package/build/codemods/__tests__/codemod_0003-rename_find_paginated.test.d.ts +0 -2
- package/build/codemods/__tests__/codemod_0003-rename_find_paginated.test.d.ts.map +0 -1
- package/build/codemods/__tests__/codemod_0003-rename_find_paginated.test.js +0 -9
- package/build/codemods/__tests__/codemod_0003-rename_find_paginated.test.js.map +0 -1
- package/build/codemods/codemod_0001-rename_has_async_property.d.ts +0 -7
- package/build/codemods/codemod_0001-rename_has_async_property.d.ts.map +0 -1
- package/build/codemods/codemod_0002-rename_async_query_fields.d.ts +0 -7
- package/build/codemods/codemod_0002-rename_async_query_fields.d.ts.map +0 -1
- package/build/codemods/codemod_0003-rename_find_paginated.d.ts +0 -7
- package/build/codemods/codemod_0003-rename_find_paginated.d.ts.map +0 -1
- package/build/codemods/index.d.ts +0 -9
- package/build/codemods/index.d.ts.map +0 -1
- package/build/codemods/index.test.d.ts +0 -2
- package/build/codemods/index.test.d.ts.map +0 -1
- package/build/codemods/index.test.js +0 -24
- package/build/codemods/index.test.js.map +0 -1
- package/build/config.d.ts +0 -172
- package/build/config.d.ts.map +0 -1
- package/build/config.test.d.ts +0 -2
- package/build/config.test.d.ts.map +0 -1
- package/build/config.test.js +0 -78
- package/build/config.test.js.map +0 -1
- package/build/docs/cache.d.ts +0 -14
- package/build/docs/cache.d.ts.map +0 -1
- package/build/docs/cache.test.d.ts +0 -2
- package/build/docs/cache.test.d.ts.map +0 -1
- package/build/docs/cache.test.js +0 -47
- package/build/docs/cache.test.js.map +0 -1
- package/build/docs/generate-metadata-docs.d.ts +0 -6
- package/build/docs/generate-metadata-docs.d.ts.map +0 -1
- package/build/docs/index.d.ts +0 -9
- package/build/docs/index.d.ts.map +0 -1
- package/build/docs/index.test.d.ts +0 -2
- package/build/docs/index.test.d.ts.map +0 -1
- package/build/docs/index.test.js +0 -100
- package/build/docs/index.test.js.map +0 -1
- package/build/docs/markdown.d.ts.map +0 -1
- package/build/docs/markdown.test.d.ts +0 -2
- package/build/docs/markdown.test.d.ts.map +0 -1
- package/build/docs/markdown.test.js +0 -280
- package/build/docs/markdown.test.js.map +0 -1
- package/build/docs/parsing.d.ts +0 -42
- package/build/docs/parsing.d.ts.map +0 -1
- package/build/docs/parsing.test.d.ts +0 -2
- package/build/docs/parsing.test.d.ts.map +0 -1
- package/build/docs/parsing.test.js +0 -190
- package/build/docs/parsing.test.js.map +0 -1
- package/build/findEntityScopes.d.ts +0 -11
- package/build/findEntityScopes.d.ts.map +0 -1
- package/build/foreignKeyOrdering.d.ts +0 -3
- package/build/foreignKeyOrdering.d.ts.map +0 -1
- package/build/generate.d.ts +0 -12
- package/build/generate.d.ts.map +0 -1
- package/build/generateEntitiesFile.d.ts +0 -7
- package/build/generateEntitiesFile.d.ts.map +0 -1
- package/build/generateEntityCodegenFile.d.ts +0 -11
- package/build/generateEntityCodegenFile.d.ts.map +0 -1
- package/build/generateEntityFile.d.ts +0 -6
- package/build/generateEntityFile.d.ts.map +0 -1
- package/build/generateEntityTestFile.d.ts +0 -6
- package/build/generateEntityTestFile.d.ts.map +0 -1
- package/build/generateEnumFile.d.ts +0 -5
- package/build/generateEnumFile.d.ts.map +0 -1
- package/build/generateFactoriesFiles.d.ts +0 -4
- package/build/generateFactoriesFiles.d.ts.map +0 -1
- package/build/generateFlushFunction.d.ts.map +0 -1
- package/build/generateMetadataFile.d.ts +0 -5
- package/build/generateMetadataFile.d.ts.map +0 -1
- package/build/generatePgEnumFile.d.ts +0 -5
- package/build/generatePgEnumFile.d.ts.map +0 -1
- package/build/index.d.ts +0 -11
- package/build/index.d.ts.map +0 -1
- package/build/inheritance.d.ts +0 -10
- package/build/inheritance.d.ts.map +0 -1
- package/build/keywords.d.ts +0 -2
- package/build/keywords.d.ts.map +0 -1
- package/build/loadMetadata.d.ts +0 -26
- package/build/loadMetadata.d.ts.map +0 -1
- package/build/logger.d.ts +0 -10
- package/build/logger.d.ts.map +0 -1
- package/build/scanEntityFiles.d.ts +0 -5
- package/build/scanEntityFiles.d.ts.map +0 -1
- package/build/sortForeignKeys.d.ts.map +0 -1
- package/build/symbols.d.ts +0 -108
- package/build/symbols.d.ts.map +0 -1
- package/build/tsdoc.d.ts +0 -12
- package/build/tsdoc.d.ts.map +0 -1
- package/build/utils.d.ts +0 -31
- package/build/utils.d.ts.map +0 -1
- package/build/write-json-schema.d.ts +0 -2
- package/build/write-json-schema.d.ts.map +0 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: joist-reactive-hint
|
|
3
|
+
description: Explains how Joist reactive hints work and what they cost to run — a hint is both a reverse-reactivity trigger (which roots re-run when a hinted field changes) and a forward load hint (what gets populated before the lambda runs), so a rule can pull an entire child collection into memory even when its lambda only reads a parent field. Covers followReverseHint, the two rule shapes (child-rooted reacting up to a parent vs parent-rooted reacting down into a child collection), and how to avoid surprise O(children) loads. Use when reasoning about the performance of a reactive rule/field/property (addRule, hasReactiveField, hasReactiveProperty, hasReactiveReference), when a validation rule seems to load far more rows than expected, when deciding whether to put a field in a reactive hint vs load it on-demand, or when a hot-path mutation (e.g. a bulk job) triggers surprise N×M loads. Keywords: reactive hint, load hint, reverse reactivity, followReverseHint, reverseSubHint, m2o vs o2m reactivity, over-invalidation, populate, "a parent rule that hints a child collection loads the whole collection".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Joist reactive hints
|
|
7
|
+
|
|
8
|
+
Reactive hints declare the fields a rule, `ReactiveField`, `ReactiveProperty`, or `ReactiveReference`
|
|
9
|
+
depends on. A hint does **double duty**: Joist walks it in **reverse** to find *which* roots to re-run
|
|
10
|
+
when a hinted field changes, then uses it **forward as a load hint** to populate that data on each root
|
|
11
|
+
before the lambda runs. Most hints are cheap — but either direction can pull an entire child collection
|
|
12
|
+
into memory, and the *forward* load is the surprising one, so both are the focus below.
|
|
13
|
+
|
|
14
|
+
## TL;DR
|
|
15
|
+
|
|
16
|
+
A reactive rule/field is declared on a **root entity** with a hint. There are two shapes, and they
|
|
17
|
+
cost very differently.
|
|
18
|
+
|
|
19
|
+
**Shape #1 — rule on the child, hint reaches *up* to the parent** (e.g. a rule on `Book` with
|
|
20
|
+
`{ author: ["currentDraftBook"] }`). When `Author.currentDraftBook` changes, Joist walks the hint in
|
|
21
|
+
reverse (`followReverseHint`), reversing the `Book.author` m2o into the `Author.books` o2m and
|
|
22
|
+
**loading the whole collection** to enumerate the child roots, then re-runs each book's rule.
|
|
23
|
+
`O(children)` per parent-field change. This is **intuitive**: "change the author, and all of its books
|
|
24
|
+
re-check."
|
|
25
|
+
|
|
26
|
+
**Shape #2 — rule on the parent, hint reaches *down* into a child collection** (e.g. a rule on `Author`
|
|
27
|
+
with `{ books: ["title"] }`). When a single `Book.title` changes, the reverse walk is **cheap** — one
|
|
28
|
+
`book.author` m2o hop to the one author. But the reactive hint is **also a load hint**: before running
|
|
29
|
+
the author's lambda, Joist populates `author.books`, pulling the **entire** collection into memory —
|
|
30
|
+
*even if the lambda only reads `author.status` and never touches the books*.
|
|
31
|
+
|
|
32
|
+
> The surprising one is **Shape #2**. `{ books: ["title"] }` reads like "react to book titles," but it
|
|
33
|
+
> *also* means "load all of this author's books every time this rule fires." A lambda whose body is just
|
|
34
|
+
> `return a.status === "active" ? undefined : "…"` still pays to load `author.books`, because the
|
|
35
|
+
> **hint, not the lambda body, decides what gets loaded**.
|
|
36
|
+
|
|
37
|
+
Neither shows up in tests (behavior is correct; only throughput/memory suffer). Both bite bulk/hot-path
|
|
38
|
+
jobs at scale — Shape #2 especially, because it looks cheap from the reverse-reactivity side.
|
|
39
|
+
|
|
40
|
+
## Why: reverse reactivity *and* load hints
|
|
41
|
+
|
|
42
|
+
A hinted-field change triggers two steps, both in `joist-core/build/reactivity/reactiveHints.js`:
|
|
43
|
+
|
|
44
|
+
**Step 1 — find the roots (reverse walk).** `followReverseHint` starts at the changed entity and walks
|
|
45
|
+
the hint in reverse, reversing each *traversed relation* (via `reverseSubHint`):
|
|
46
|
+
|
|
47
|
+
- Reverse of an **m2o** (`Book.author`) is the **o2m** (`Author.books`) — so a **child-rooted** rule
|
|
48
|
+
reacting to a parent field loads the whole sibling collection just to list the roots (Shape #1).
|
|
49
|
+
- Reverse of an **o2m** (`Author.books`) is the **m2o** (`Book.author`) — so a **parent-rooted** rule
|
|
50
|
+
reacting to a child field finds exactly one root per changed child (Shape #2's reverse is cheap).
|
|
51
|
+
- A **leaf field** (primitive / enum / m2o read as a value, e.g. `["title"]`, `["currentDraftBook"]`) is
|
|
52
|
+
only a "react to this field changing" trigger; it is **not** traversed into.
|
|
53
|
+
|
|
54
|
+
**Step 2 — load the hint (forward populate).** Before running each root's lambda, Joist populates the
|
|
55
|
+
reactive hint *as a load hint* on that root so the lambda can read it synchronously. For a parent-rooted
|
|
56
|
+
rule with `{ books: [...] }`, that is effectively `author.populate("books")` — the **full collection** —
|
|
57
|
+
**regardless of what the lambda actually reads**. This is why Shape #2 is expensive even though its
|
|
58
|
+
reverse walk touched only one author: the cost is in the forward load, not the reverse walk.
|
|
59
|
+
|
|
60
|
+
So `{ author: ["currentDraftBook"] }` on `Book` compiles to a reactive target
|
|
61
|
+
`{ entity: Author, fields: ["currentDraftBook"], path: ["books"] }` — "when `Author.currentDraftBook`
|
|
62
|
+
changes, load `Author.books` and re-validate each" — while `{ books: ["title"] }` on `Author` loads
|
|
63
|
+
`author.books` on every fire so the rule can run at all. Either way Joist over-invalidates for
|
|
64
|
+
correctness: every sibling is re-checked/loaded even when only one could change the outcome.
|
|
65
|
+
|
|
66
|
+
## The old/new-value shortcut — and when it does NOT apply
|
|
67
|
+
|
|
68
|
+
`followReverseHint` has a reference-history optimization (`getInstanceData(c).getReferenceHistory`)
|
|
69
|
+
that pulls in **old + new** values — but only when the **step being walked is itself an m2o/poly**
|
|
70
|
+
(the "`Book.author` moved to a new `Author`" reparent case). It exists so a reparent re-validates
|
|
71
|
+
both the old and new parent.
|
|
72
|
+
|
|
73
|
+
It does **not** turn a child→`parent.field` reaction into a cheap old/new lookup: there, the walked
|
|
74
|
+
step is the `books` **o2m**, so Joist takes the `relation.load()` (full-collection) branch. The
|
|
75
|
+
`currentDraftBook` field's own m2o-ness is irrelevant — it's reacted to *as a field*, not *traversed*.
|
|
76
|
+
|
|
77
|
+
Rule of thumb for the cost of one changed field:
|
|
78
|
+
- reverse step is an **o2m/o2o** (you're on the child, parent-field changed) → **loads the full
|
|
79
|
+
sibling collection**.
|
|
80
|
+
- reverse step is an **m2o/poly** being reparented → loads a couple entities (current + history).
|
|
81
|
+
|
|
82
|
+
## How to spot / avoid it
|
|
83
|
+
|
|
84
|
+
- **The hint decides the load, not the lambda body.** A parent rule with `{ books: [...] }` loads
|
|
85
|
+
*every* book on *every* fire, even if the lambda only reads the parent's own fields. If the rule does
|
|
86
|
+
not actually need the collection, do not hint it; if it does, know you are paying `O(children)` memory
|
|
87
|
+
per fire.
|
|
88
|
+
- **For SQL-derived fields, split the hint with `hasAsyncReactiveField`.** It takes two hints — a
|
|
89
|
+
`loadHint`, whose data is populated into memory and passed to the lambda, and a `reactiveHint`, whose
|
|
90
|
+
data only *triggers* recalculation but is **not** loaded into memory (the lambda recomputes from SQL
|
|
91
|
+
instead). Put the large child collections you merely need to react to in the `reactiveHint` so a
|
|
92
|
+
child-field change still recomputes the value without pulling every sibling into memory.
|
|
93
|
+
- **Root the rule where the frequently-changing field lives.** A rule on `Book` reacting to
|
|
94
|
+
`author.currentDraftBook` reloads all siblings; the same check rooted on `Author` reacting to its
|
|
95
|
+
own `["currentDraftBook"]` needs no reverse collection-load (but then detecting anything about the
|
|
96
|
+
*children* forces you to scan them anyway — so this only helps if the check is about the parent).
|
|
97
|
+
- **Only react to fields that change rarely.** Put a field in the reactive hint only if you truly
|
|
98
|
+
need to re-run when it changes. If you just need to *read* a value, `await rel.load()` it on
|
|
99
|
+
demand inside the rule and leave it out of the hint. Reserve the hint for the rare-change trigger
|
|
100
|
+
that actually needs to fire the rule.
|
|
101
|
+
- **Watch bulk/hot-path mutations.** If a job sets some `parent.fk` once per child (e.g. a bulk
|
|
102
|
+
copy job setting `author.currentDraftBook` per book), any child-rooted rule reacting to that fk
|
|
103
|
+
reloads every child's siblings — the same `O(children × siblings)` you'd get from an explicit
|
|
104
|
+
`parent.children.load()` in the loop.
|
|
105
|
+
|
|
106
|
+
## Worked examples
|
|
107
|
+
|
|
108
|
+
### Shape #2 — the surprising one: a parent rule hinting into a child collection
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// ❌ Rooted on Author. The hint `{ books: ["title"] }` reads like "react to book titles" — but it is
|
|
112
|
+
// ALSO a load hint, so every time ANY book's title changes, Joist loads the author's ENTIRE `books`
|
|
113
|
+
// collection into memory to run this rule, even though the lambda only looks at the author's status.
|
|
114
|
+
authorConfig.addRule(["status", { books: ["title"] }], (a) => {
|
|
115
|
+
return a.status === "active" ? undefined : "inactive authors need review";
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ✅ The rule only depends on the author's own status, so hint only that — no books are loaded.
|
|
119
|
+
authorConfig.addRule("status", (a) => {
|
|
120
|
+
return a.status === "active" ? undefined : "inactive authors need review";
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If the rule genuinely must react to a child field, keep the hint — but know each fire loads the whole
|
|
125
|
+
collection, so reserve it for collections the lambda actually reads and for child fields that change
|
|
126
|
+
rarely.
|
|
127
|
+
|
|
128
|
+
### Shape #1 — intuitive but still O(children): a child rule reacting to a parent field
|
|
129
|
+
|
|
130
|
+
Invariant wanted: "an unpublished book must be one of its author's tracked books — its `favoriteBook` or
|
|
131
|
+
`currentDraftBook`."
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// ❌ Simple but O(books × siblings) on the bulk copy-job hot path:
|
|
135
|
+
// the job sets author.currentDraftBook per book -> reverse-loads author.books each time.
|
|
136
|
+
bookConfig.addRule(
|
|
137
|
+
["publishedAt", { author: ["favoriteBook", "currentDraftBook"] }],
|
|
138
|
+
(b) => { ... },
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// ✅ React only to favoriteBook (which changes rarely — on publish, not per draft edit);
|
|
142
|
+
// read currentDraftBook on-demand so it's not a reactive dependency.
|
|
143
|
+
bookConfig.addRule(["publishedAt", { author: ["favoriteBook"] }], async (b) => {
|
|
144
|
+
if (b.publishedAt) return;
|
|
145
|
+
const author = b.author.get;
|
|
146
|
+
const favorite = author.favoriteBook.get;
|
|
147
|
+
const draft = await author.currentDraftBook.load(); // read, don't react
|
|
148
|
+
if (b !== favorite && b !== draft) return `${b} is unpublished but neither the favorite nor current draft`;
|
|
149
|
+
});
|
|
150
|
+
```
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: joist-test-factories
|
|
3
|
+
description: Use when writing or refactoring Joist ORM tests with newTestInstance factories, DeepNew, run or makeRun, toMatchEntity, and nested Given graphs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Joist Test Factories
|
|
7
|
+
|
|
8
|
+
Write isolated, succinct tests that use Joist's factories for setup, retain
|
|
9
|
+
typed entity references through the action, and assert those same entities
|
|
10
|
+
with `toMatchEntity`.
|
|
11
|
+
|
|
12
|
+
Authoritative references:
|
|
13
|
+
|
|
14
|
+
- <https://joist-orm.io/testing/test-factories/>
|
|
15
|
+
- <https://joist-orm.io/testing/entity-matcher/>
|
|
16
|
+
- <https://joist-orm.io/testing/test-utils/>
|
|
17
|
+
- <https://joist-orm.io/goals/great-tests/>
|
|
18
|
+
|
|
19
|
+
## Non-Negotiable Rules
|
|
20
|
+
|
|
21
|
+
1. Create initial Given state with test factories. Never use the same function
|
|
22
|
+
or API operation under test to arrange its own preconditions.
|
|
23
|
+
2. Set only fields and relationships that define the boundary case. Let the
|
|
24
|
+
factories supply unrelated required values and dependencies.
|
|
25
|
+
3. Keep direct `const` references to entities that participate in the action or
|
|
26
|
+
assertions.
|
|
27
|
+
4. Keep using the factory-created `DeepNew` graph after same-`EntityManager`
|
|
28
|
+
actions or actions run through Joist's test `run` helper. Do not reload the
|
|
29
|
+
same rows merely to assert against them.
|
|
30
|
+
5. Use `run` or the project's `makeRun` wrapper when production code needs an
|
|
31
|
+
isolated `EntityManager`; it flushes Given state and mirrors the callback's
|
|
32
|
+
flushed Joist writes into the original test graph.
|
|
33
|
+
6. Assert entity state and relationships with `toMatchEntity`.
|
|
34
|
+
7. Prefer focused tests for one behavior over a single scenario that exercises
|
|
35
|
+
unrelated updates at several graph levels.
|
|
36
|
+
|
|
37
|
+
## Given, When, Then
|
|
38
|
+
|
|
39
|
+
Structure every test as "Given the state of the world is X, When action Y
|
|
40
|
+
happens, Then the state of the world is Z": the factory-created graph is the
|
|
41
|
+
Given, the code under test is the When, and `toMatchEntity` proves the Then.
|
|
42
|
+
Mark the three phases with `// Given`, `// When`, `// Then` comments:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
it.withCtx("updates a book", async (ctx) => {
|
|
46
|
+
// Given an author with one book
|
|
47
|
+
const author = newAuthor(ctx.em, {
|
|
48
|
+
books: [{ title: "Before" }],
|
|
49
|
+
});
|
|
50
|
+
const [book] = author.books.get;
|
|
51
|
+
|
|
52
|
+
// When we update the book's title (updateBook owns and flushes its production unit of work)
|
|
53
|
+
await run(ctx, (ctx) => updateBook(ctx, { id: book.id, title: "After" }));
|
|
54
|
+
|
|
55
|
+
// Then the book's title is changed
|
|
56
|
+
expect(author).toMatchEntity({ books: [{ title: "After" }] });
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The factory owns the Given defaults. `run` provides production isolation and
|
|
61
|
+
mirrors flushed writes. The callback still owns its production unit of work.
|
|
62
|
+
`toMatchEntity` owns the Then's entity-aware assertions.
|
|
63
|
+
|
|
64
|
+
## Factories Own Given State
|
|
65
|
+
|
|
66
|
+
Do not call the same code under test to arrange its own preconditions:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// Wrong: saveAuthor is both setup and the behavior under test.
|
|
70
|
+
const created = await saveAuthor(ctx, {
|
|
71
|
+
firstName: "a1",
|
|
72
|
+
books: [{ title: "Before" }],
|
|
73
|
+
});
|
|
74
|
+
const author = await created.author;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This couples setup to the behavior under test, can reproduce the same bug in
|
|
78
|
+
both phases, and loses the ergonomic `DeepNew` type.
|
|
79
|
+
|
|
80
|
+
Use a factory instead:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const author = newAuthor(ctx.em, {
|
|
84
|
+
books: [{ title: "Before" }],
|
|
85
|
+
});
|
|
86
|
+
const [book] = author.books.get;
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Factories are test-only tools. Never call them from production code.
|
|
90
|
+
Using another production API for Given state can be valid in an integration
|
|
91
|
+
test when that API's authorization, hooks, defaults, or events are part of the
|
|
92
|
+
scenario. This should be intentional, not the default way to create rows.
|
|
93
|
+
|
|
94
|
+
## Keep Given State Minimal
|
|
95
|
+
|
|
96
|
+
Every explicit factory option should answer: "Why does this test need this
|
|
97
|
+
value?"
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// Wrong: most values are unrelated to changing one title.
|
|
101
|
+
const author = newAuthor(ctx.em, {
|
|
102
|
+
firstName: "Ann",
|
|
103
|
+
lastName: "Smith",
|
|
104
|
+
age: 40,
|
|
105
|
+
books: [
|
|
106
|
+
{
|
|
107
|
+
title: "Before",
|
|
108
|
+
order: 1,
|
|
109
|
+
published: false,
|
|
110
|
+
reviews: [],
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// Right: only the value being changed is specified.
|
|
118
|
+
const author = newAuthor(ctx.em, {
|
|
119
|
+
books: [{ title: "Before" }],
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Specify additional values only when they establish the scenario. Examples:
|
|
124
|
+
|
|
125
|
+
- Two `{}` children establish collection cardinality.
|
|
126
|
+
- Distinct sort orders may be necessary for a parent/sort-order unique key.
|
|
127
|
+
- An initial value is necessary when the assertion proves that it changed.
|
|
128
|
+
- A relation override is necessary when the identity of that relation matters.
|
|
129
|
+
|
|
130
|
+
Do not copy production payloads into factory opts. Factory opts describe the
|
|
131
|
+
minimum database state before the action, not every field the action accepts.
|
|
132
|
+
|
|
133
|
+
## Build Graphs in One Factory Call
|
|
134
|
+
|
|
135
|
+
Prefer a top-level factory with nested opts when it clearly describes the
|
|
136
|
+
scenario:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const author = newAuthor(ctx.em, {
|
|
140
|
+
books: [{ title: "First", reviews: [{ rating: 5 }] }, { title: "Second" }],
|
|
141
|
+
});
|
|
142
|
+
const [firstBook, secondBook] = author.books.get;
|
|
143
|
+
const [review] = firstBook.reviews.get;
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This is usually clearer than creating each row separately and wiring every
|
|
147
|
+
required relation by hand. Separate factory calls are appropriate when the
|
|
148
|
+
test's behavior is specifically about how independently created entities
|
|
149
|
+
relate.
|
|
150
|
+
|
|
151
|
+
Factories recursively fill required primitives and relations. They also reuse
|
|
152
|
+
an obvious existing entity when exactly one candidate exists. Use factory
|
|
153
|
+
controls intentionally:
|
|
154
|
+
|
|
155
|
+
- Pass an entity directly to force a specific relation.
|
|
156
|
+
- Pass `{ use: entity }` to nominate an existing entity throughout a factory
|
|
157
|
+
scope.
|
|
158
|
+
- Pass `{}` for a relation when a new related entity is required.
|
|
159
|
+
- Use `useFactoryDefaults: false` sparingly; frequent use means the factory
|
|
160
|
+
defaults may be too opinionated.
|
|
161
|
+
- Use `useFactoryDefaults: "none"` only for tests explicitly exercising invalid
|
|
162
|
+
or incomplete state.
|
|
163
|
+
|
|
164
|
+
## Retain Entity References
|
|
165
|
+
|
|
166
|
+
Immediately name entities used by the action or assertions:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const author = newAuthor(ctx.em, {
|
|
170
|
+
books: [{}, {}],
|
|
171
|
+
});
|
|
172
|
+
const [updatedBook, deletedBook] = author.books.get;
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Do not unnecessarily rediscover Given entities later by index, query, ID, or
|
|
176
|
+
mutation result. Newly created entities can come from the action result, and a
|
|
177
|
+
query result is appropriate when querying is the behavior under test.
|
|
178
|
+
|
|
179
|
+
For complicated cross-references in one factory graph, use factory IDs:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const author = newAuthor(ctx.em, {
|
|
183
|
+
books: [
|
|
184
|
+
{ is: "b#1", title: "First" },
|
|
185
|
+
{ is: "b#2", prequel: "b#1", title: "Second" },
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
const [firstBook, secondBook] = author.books.get;
|
|
189
|
+
|
|
190
|
+
expect(secondBook).toMatchEntity({ prequel: firstBook });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The `factories` proxy is also available when direct relation destructuring is
|
|
194
|
+
awkward:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
newAuthor(ctx.em, { books: [{}, {}] });
|
|
198
|
+
const { a1, b1, b2 } = factories;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The proxy resolves against the most recently used `EntityManager`, so avoid it
|
|
202
|
+
in tests with multiple active test entity managers.
|
|
203
|
+
|
|
204
|
+
Prefer ordinary named constants when the graph is small; they make the test's
|
|
205
|
+
roles clearer than numeric factory IDs.
|
|
206
|
+
|
|
207
|
+
## DeepNew Means Async-Free Graph Access
|
|
208
|
+
|
|
209
|
+
Follow the signature generated by the project's Joist version. Factories that
|
|
210
|
+
return `DeepNew<Entity>` provide the loaded graph ergonomics used here:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
export function newAuthor(em: EntityManager, opts: FactoryOpts<Author> = {}): DeepNew<Author> {
|
|
214
|
+
return newTestInstance(em, Author, opts, {});
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`DeepNew` is a loadedness type for the factory-created graph, so setup and
|
|
219
|
+
assertions can use `.get` without `await`:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const author = newAuthor(ctx.em, { books: [{}] });
|
|
223
|
+
const [book] = author.books.get;
|
|
224
|
+
|
|
225
|
+
expect(author).toMatchEntity({ books: [book] });
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Avoid this boilerplate:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const books = await author.books.load();
|
|
232
|
+
const reloadedAuthor = await ctx.em.load(Author, author.id);
|
|
233
|
+
const reloadedBooks = await reloadedAuthor.books.load();
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`DeepNew` does not synchronize writes from another `EntityManager`. If a
|
|
237
|
+
relation was not part of the factory-created graph, use an intentional
|
|
238
|
+
populate, or use Joist's `run` helper when testing a separate production unit
|
|
239
|
+
of work. Do not silence a legitimate unloaded-relation boundary with arbitrary
|
|
240
|
+
test-only loads.
|
|
241
|
+
|
|
242
|
+
## Use run for Production Isolation
|
|
243
|
+
|
|
244
|
+
Joist's `run(ctx, fn)` executes `fn` with a fresh production-style context and
|
|
245
|
+
`EntityManager`. Before the callback, it flushes the test factory graph. As the
|
|
246
|
+
callback flushes Joist writes, `RunPlugin` mirrors those writes into the
|
|
247
|
+
original test `EntityManager`. It does not call `EntityManager.refresh`. The
|
|
248
|
+
callback result is also mapped back to entities from the original test
|
|
249
|
+
`EntityManager`.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
const author = newAuthor(ctx.em, { books: [{ title: "Before" }] });
|
|
253
|
+
const [book] = author.books.get;
|
|
254
|
+
|
|
255
|
+
// updateBook must own and flush its production unit of work.
|
|
256
|
+
await run(ctx, (ctx) => updateBook(ctx, { id: book.id, title: "After" }));
|
|
257
|
+
|
|
258
|
+
expect(book).toMatchEntity({ title: "After" });
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Many applications expose a project-specific helper created with `makeRun`,
|
|
262
|
+
such as `runMutation` or `runService`. Use that helper instead of manually
|
|
263
|
+
constructing a second `EntityManager`.
|
|
264
|
+
|
|
265
|
+
`run` deliberately does not flush the callback's `EntityManager`; production
|
|
266
|
+
code under test must own its normal unit-of-work boundary. Unflushed changes,
|
|
267
|
+
direct SQL, and writes outside Joist are not guaranteed to be mirrored into the
|
|
268
|
+
test graph.
|
|
269
|
+
|
|
270
|
+
Do not write:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
const result = await runMutation(ctx, () => updateBookInput(book.id));
|
|
274
|
+
const savedBook = await result.book;
|
|
275
|
+
const books = await savedBook.author.load().then((author) => author.books.load());
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Keep using `author` and `book`. They are the stable test references.
|
|
279
|
+
|
|
280
|
+
Call `em.flush()` directly only when the test intentionally needs a persistence
|
|
281
|
+
boundary and its helper does not provide one. Pure entity tests should usually
|
|
282
|
+
avoid unnecessary flushes.
|
|
283
|
+
|
|
284
|
+
## Register toMatchEntity
|
|
285
|
+
|
|
286
|
+
Examples assume `toMatchEntity` is imported from the `joist-orm/tests`
|
|
287
|
+
entry point and registered in the project's test setup:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { toMatchEntity } from "joist-orm/tests";
|
|
291
|
+
|
|
292
|
+
expect.extend({ toMatchEntity });
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Use the runner-specific setup and types from the installed Joist version.
|
|
296
|
+
|
|
297
|
+
## Assert with toMatchEntity
|
|
298
|
+
|
|
299
|
+
`toMatchEntity` provides `toMatchObject`-style subset assertions while
|
|
300
|
+
understanding Joist references, collections, properties, reactive fields, and
|
|
301
|
+
entity identity. It also produces concise entity IDs in diffs.
|
|
302
|
+
|
|
303
|
+
`toMatchEntity` synchronously unwraps loaded relations through `.get`; it does
|
|
304
|
+
not query or asynchronously load missing relations. Ensure the asserted graph
|
|
305
|
+
is loaded by the factory, an intentional populate, or `RunPlugin`.
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
expect(author).toMatchEntity({
|
|
309
|
+
firstName: "Ann",
|
|
310
|
+
books: [book],
|
|
311
|
+
});
|
|
312
|
+
expect(book).toMatchEntity({ title: "After" });
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Pass retained entity constants directly for relationship identity. Use nested
|
|
316
|
+
object literals when the nested values themselves are the assertion:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
expect(author).toMatchEntity({
|
|
320
|
+
books: [{ title: "First" }, { title: "Second" }],
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
For a hard delete, assert both the surviving collection and deletion state
|
|
325
|
+
when deletion semantics matter:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
expect(author).toMatchEntity({ books: [updatedBook] });
|
|
329
|
+
expect(deletedBook).toMatchEntity({ isDeletedEntity: true });
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Do not map entity graphs into temporary POJOs just to use `toEqual`:
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
// Wrong.
|
|
336
|
+
expect((await author.books.load()).map((book) => ({ id: book.id, title: book.title }))).toEqual([
|
|
337
|
+
{ id: updatedBook.id, title: "After" },
|
|
338
|
+
]);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Keep Tests Focused
|
|
342
|
+
|
|
343
|
+
One test should describe one coherent boundary. Split a large graph mutation
|
|
344
|
+
into separate tests when failures would otherwise have several unrelated
|
|
345
|
+
causes:
|
|
346
|
+
|
|
347
|
+
- Updating/deleting members of a parent collection.
|
|
348
|
+
- Updating/deleting grandchildren.
|
|
349
|
+
- Creating a new nested entity.
|
|
350
|
+
- Clearing a collection.
|
|
351
|
+
- Preserving an omitted relation.
|
|
352
|
+
|
|
353
|
+
Combining an update and delete can be appropriate when they jointly exercise
|
|
354
|
+
one incremental-collection contract. Avoid a kitchen-sink test that performs
|
|
355
|
+
step updates, ingredient updates, note updates, and several unrelated scalar
|
|
356
|
+
changes in one action.
|
|
357
|
+
|
|
358
|
+
Focused tests produce smaller Given graphs, clearer constants, and useful
|
|
359
|
+
failure messages.
|
|
360
|
+
|
|
361
|
+
## Factory Defaults
|
|
362
|
+
|
|
363
|
+
Customize a generated factory only for defaults that make entities valid by
|
|
364
|
+
default across the suite:
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
export function newAuthor(em: EntityManager, opts: FactoryOpts<Author> = {}): DeepNew<Author> {
|
|
368
|
+
return newTestInstance(em, Author, opts, {
|
|
369
|
+
age: 40,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Use `testIndex` for deterministic unique values when a database unique
|
|
375
|
+
constraint requires them. Add collection defaults only for genuine suite-wide
|
|
376
|
+
validity rules, such as every valid author requiring at least one book.
|
|
377
|
+
|
|
378
|
+
Custom options such as `withSignedContract` can package a commonly repeated
|
|
379
|
+
graph, but use them sparingly. A reader should not need to inspect a factory to
|
|
380
|
+
understand the values directly asserted by the test.
|
|
381
|
+
|
|
382
|
+
If factory behavior is surprising, enable `useLogging: true` for that call or
|
|
383
|
+
temporarily enable global factory logging. Diagnose the factory scope and
|
|
384
|
+
reuse decisions instead of replacing factories with manual setup.
|
|
385
|
+
|
|
386
|
+
## Review Checklist
|
|
387
|
+
|
|
388
|
+
- Test reads as Given, When, Then, with the code under test isolated as a single
|
|
389
|
+
When.
|
|
390
|
+
- Given state uses minimal factory opts and does not invoke the behavior under
|
|
391
|
+
test.
|
|
392
|
+
- Important entities have direct, role-based `const` names.
|
|
393
|
+
- Separate production units of work use `run`/`makeRun` and flush normally.
|
|
394
|
+
- Assertions reuse loaded factory entities with `toMatchEntity`.
|
|
395
|
+
- No unnecessary reloads, `load()` calls, assertion awaits, or direct flushes
|
|
396
|
+
remain.
|
|
397
|
+
- Each test covers one coherent boundary.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: joist-upsert
|
|
3
|
+
description: Implement partial-update / RPC / GraphQL save endpoints with Joist using setPartial, createPartial, em.upsert, and incremental collection ops. Use when a create/update accepts a subset of fields, treats null as "unset", or saves a parent plus a mix of new and existing children.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Managed by joist-codegen. Do not edit by hand; re-run codegen to update. -->
|
|
7
|
+
|
|
8
|
+
# Joist partial updates & upsert
|
|
9
|
+
|
|
10
|
+
These APIs exist for "partial update" endpoints (REST/GraphQL/gRPC) where the
|
|
11
|
+
input is loosely typed (`string | null | undefined`) and follows the
|
|
12
|
+
conventions:
|
|
13
|
+
|
|
14
|
+
- A subset of fields may be sent; omitted fields are left as-is.
|
|
15
|
+
- `null` means "unset this field".
|
|
16
|
+
- Children collections can be updated incrementally.
|
|
17
|
+
|
|
18
|
+
Joist's normal `em.create` / `Entity.set` are intentionally strict and won't
|
|
19
|
+
accept `string | null | undefined`. The partial variants opt into the looser
|
|
20
|
+
semantics.
|
|
21
|
+
|
|
22
|
+
## `setPartial`
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// firstName is typed `string | null | undefined` (e.g. from a GraphQL input)
|
|
26
|
+
const author = await em.load(Author, "a:1");
|
|
27
|
+
author.setPartial({ firstName }); // compiles; `set` would not
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Semantics, per field:
|
|
31
|
+
|
|
32
|
+
- Required field (`firstName`): value updates it; `undefined` does nothing;
|
|
33
|
+
`null` is a **validation error** (required field can't be unset).
|
|
34
|
+
- Optional field (`lastName`): value updates it; `undefined` does nothing;
|
|
35
|
+
`null` unsets it (sets to `undefined`).
|
|
36
|
+
- Collection (`books`): `[b1]` sets it to exactly `[b1]`; `null` sets it to
|
|
37
|
+
`[]`; `undefined` does nothing.
|
|
38
|
+
|
|
39
|
+
`em.createPartial(Author, ...)` and `em.upsert(Author, ...)` share these
|
|
40
|
+
semantics.
|
|
41
|
+
|
|
42
|
+
## `em.upsert` — parent plus children
|
|
43
|
+
|
|
44
|
+
`em.upsert` saves a parent and a mix of new/existing children in one call. It is
|
|
45
|
+
**async** (unlike `em.create`) because it may issue `SELECT`s to resolve
|
|
46
|
+
existing child ids.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
await em.upsert(Author, {
|
|
50
|
+
id: "a:1", // update author 1
|
|
51
|
+
books: [
|
|
52
|
+
{ title: "new book" }, // no id -> create
|
|
53
|
+
{ id: "b:1" }, // existing, unchanged
|
|
54
|
+
{ id: "b:2", title: "updated" }, // existing, updated
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
By default a collection is set **exhaustively** — any existing child not listed
|
|
60
|
+
is removed.
|
|
61
|
+
|
|
62
|
+
## Incremental collections (`op`)
|
|
63
|
+
|
|
64
|
+
To change only some children without sending the whole collection, add an `op`
|
|
65
|
+
hint to each child:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
author.setPartial({
|
|
69
|
+
books: [{ op: "include", title: "b3" }], // adds b3, leaves existing books
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `{ op: "include", id }` — add if needed, or update an existing child
|
|
74
|
+
- `{ op: "remove", id }` — remove from the collection (no delete)
|
|
75
|
+
- `{ op: "delete", id }` — remove and `em.delete` the child
|
|
76
|
+
|
|
77
|
+
Rules and gotchas:
|
|
78
|
+
|
|
79
|
+
- If **any** child has an `op`, **all** children must have one.
|
|
80
|
+
- `op` is not a real entity field — it's only a hint on the input type.
|
|
81
|
+
- An **empty** list always clears the collection (it looks like an exhaustive
|
|
82
|
+
set), so to send "no changes" omit the collection key entirely. Alternatively
|
|
83
|
+
include a single `{ op: "incremental" }` sentinel child to force incremental
|
|
84
|
+
semantics without adding/removing anything.
|
|
85
|
+
|
|
86
|
+
## Legacy keys (soft-deprecated)
|
|
87
|
+
|
|
88
|
+
Older code used `delete: true` / `remove: true` instead of `op`; still
|
|
89
|
+
supported, but prefer `op` for new code:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
author.setPartial({
|
|
93
|
+
books: [{ id: "b:1", delete: true }, { id: "b:2", remove: true }, { id: "b:4" }],
|
|
94
|
+
});
|
|
95
|
+
```
|