@morscherlab/mint-sdk 1.0.0-rc.1 → 1.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/README.md +9 -1
  2. package/dist/__tests__/components/LcmsSequenceTable.test.d.ts +1 -0
  3. package/dist/__tests__/components/ProgressBar.test.d.ts +1 -0
  4. package/dist/__tests__/components/RackEditor.test.d.ts +1 -0
  5. package/dist/__tests__/components/SequenceProgressBar.test.d.ts +1 -0
  6. package/dist/__tests__/composables/useExperimentSamples.test.d.ts +1 -0
  7. package/dist/__tests__/utils/instrument.test.d.ts +1 -0
  8. package/dist/__tests__/utils/lcms.test.d.ts +1 -0
  9. package/dist/__tests__/utils/permissions.test.d.ts +1 -0
  10. package/dist/__tests__/utils/rack.test.d.ts +1 -0
  11. package/dist/{auth-CBG3bWEc.js → auth-B7g4J4ZF.js} +99 -5
  12. package/dist/auth-B7g4J4ZF.js.map +1 -0
  13. package/dist/components/AppSidebar.vue.d.ts +3 -3
  14. package/dist/components/AutoGroupModal.vue.d.ts +1 -1
  15. package/dist/components/BaseCheckbox.vue.d.ts +1 -1
  16. package/dist/components/BaseToggle.vue.d.ts +2 -2
  17. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
  18. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
  19. package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +1 -1
  20. package/dist/components/ControlWorkspaceView.vue.d.ts +3 -3
  21. package/dist/components/DoseDesignWorkspaceView.vue.d.ts +2 -2
  22. package/dist/components/FormulaInput.vue.d.ts +1 -1
  23. package/dist/components/InstrumentAlertLog.vue.d.ts +22 -0
  24. package/dist/components/InstrumentStateBadge.vue.d.ts +11 -0
  25. package/dist/components/InstrumentStatusCard.vue.d.ts +13 -0
  26. package/dist/components/LcmsSequenceTable.vue.d.ts +26 -0
  27. package/dist/components/PluginWorkspaceView.vue.d.ts +3 -3
  28. package/dist/components/ProgressBar.vue.d.ts +1 -0
  29. package/dist/components/RackEditor.vue.d.ts +41 -3
  30. package/dist/components/ReagentList.vue.d.ts +1 -1
  31. package/dist/components/SampleSelector.vue.d.ts +5 -2
  32. package/dist/components/SegmentedControl.vue.d.ts +2 -0
  33. package/dist/components/SequenceInput.vue.d.ts +1 -1
  34. package/dist/components/SequenceProgressBar.vue.d.ts +15 -0
  35. package/dist/components/SettingsModal.vue.d.ts +3 -1
  36. package/dist/components/TagsInput.vue.d.ts +1 -1
  37. package/dist/components/WellPlate.vue.d.ts +42 -3
  38. package/dist/components/index.d.ts +5 -0
  39. package/dist/components/index.js +3 -3
  40. package/dist/{components-5KSfsVqf.js → components-DafPc4rM.js} +2091 -1051
  41. package/dist/components-DafPc4rM.js.map +1 -0
  42. package/dist/composables/controlComponentBindings.d.ts +7 -0
  43. package/dist/composables/controlSchemaAdapters.d.ts +20 -0
  44. package/dist/composables/controlSchemaFormFields.d.ts +3 -0
  45. package/dist/composables/controlSchemaLayout.d.ts +7 -0
  46. package/dist/composables/controlSchemaNormalize.d.ts +15 -0
  47. package/dist/composables/controlSchemaUtils.d.ts +9 -0
  48. package/dist/composables/controlWorkspaceOptions.d.ts +2 -0
  49. package/dist/composables/experimentDesignData.d.ts +17 -0
  50. package/dist/composables/index.d.ts +2 -0
  51. package/dist/composables/index.js +4 -4
  52. package/dist/composables/useControlSchema.d.ts +15 -30
  53. package/dist/composables/useControlWorkspace.d.ts +5 -0
  54. package/dist/composables/useExperimentData.d.ts +11 -3
  55. package/dist/composables/useExperimentSamples.d.ts +42 -0
  56. package/dist/composables/usePlatformContext.d.ts +54 -0
  57. package/dist/{composables-D4Myb30a.js → composables-BMkPQhVK.js} +5 -3
  58. package/dist/composables-BMkPQhVK.js.map +1 -0
  59. package/dist/index.d.ts +4 -0
  60. package/dist/index.js +168 -6
  61. package/dist/index.js.map +1 -0
  62. package/dist/install.js +2 -2
  63. package/dist/instrument.d.ts +7 -0
  64. package/dist/lcms.d.ts +27 -0
  65. package/dist/permissions.d.ts +46 -0
  66. package/dist/stores/auth.d.ts +74 -2
  67. package/dist/stores/index.js +1 -1
  68. package/dist/styles.css +3316 -1216
  69. package/dist/templates/adapters.d.ts +14 -47
  70. package/dist/templates/assayLookups.d.ts +3 -0
  71. package/dist/templates/assayMatrixAdapters.d.ts +6 -0
  72. package/dist/templates/assayMatrixBuilder.d.ts +2 -0
  73. package/dist/templates/assayNormalizers.d.ts +4 -0
  74. package/dist/templates/builderDefaults.d.ts +1 -0
  75. package/dist/templates/builderIdUtils.d.ts +4 -0
  76. package/dist/templates/builderPresetControls.d.ts +10 -0
  77. package/dist/templates/builderReadUtils.d.ts +8 -0
  78. package/dist/templates/builders.d.ts +26 -63
  79. package/dist/templates/calibrationCurveAdapters.d.ts +4 -0
  80. package/dist/templates/calibrationCurveBuilder.d.ts +2 -0
  81. package/dist/templates/calibrationNormalizers.d.ts +5 -0
  82. package/dist/templates/componentBindingCatalog.d.ts +25 -0
  83. package/dist/templates/componentBindingHelpers.d.ts +17 -0
  84. package/dist/templates/componentDoseResponseProps.d.ts +3 -0
  85. package/dist/templates/componentGenericProps.d.ts +8 -0
  86. package/dist/templates/componentPlateHelpers.d.ts +5 -0
  87. package/dist/templates/componentPlateMapProps.d.ts +3 -0
  88. package/dist/templates/componentPropsFactory.d.ts +3 -0
  89. package/dist/templates/componentQpcrPlateProps.d.ts +3 -0
  90. package/dist/templates/componentTargetResolvers.d.ts +5 -0
  91. package/dist/templates/componentTemplateProps.d.ts +3 -0
  92. package/dist/templates/controlSchemaClone.d.ts +4 -0
  93. package/dist/templates/controlSchemaConstants.d.ts +10 -0
  94. package/dist/templates/controlSchemaMerge.d.ts +3 -0
  95. package/dist/templates/controlSchemaTargets.d.ts +17 -0
  96. package/dist/templates/controlSchemaTypes.d.ts +4 -0
  97. package/dist/templates/controlSchemas.d.ts +4 -4
  98. package/dist/templates/defaultBioTemplateBuilder.d.ts +2 -0
  99. package/dist/templates/doseResponseAdapters.d.ts +4 -0
  100. package/dist/templates/doseResponseBuilder.d.ts +2 -0
  101. package/dist/templates/elisaAssayCollectionBuilder.d.ts +2 -0
  102. package/dist/templates/flowCytometryAssayCollectionBuilder.d.ts +2 -0
  103. package/dist/templates/flowCytometryPanelBuilder.d.ts +2 -0
  104. package/dist/templates/flowNormalizers.d.ts +8 -0
  105. package/dist/templates/flowPanelAdapters.d.ts +4 -0
  106. package/dist/templates/index.d.ts +2 -2
  107. package/dist/templates/index.js +2 -2
  108. package/dist/templates/instrumentRunAdapterHelpers.d.ts +8 -0
  109. package/dist/templates/instrumentRunAdapters.d.ts +8 -0
  110. package/dist/templates/instrumentRunBuilder.d.ts +2 -0
  111. package/dist/templates/lcmsBatchCollectionBuilder.d.ts +2 -0
  112. package/dist/templates/plateGeometry.d.ts +4 -0
  113. package/dist/templates/plateMapAdapters.d.ts +3 -0
  114. package/dist/templates/plateMapBuilder.d.ts +2 -0
  115. package/dist/templates/presetControlSchemas.d.ts +534 -0
  116. package/dist/templates/presets.d.ts +12 -0
  117. package/dist/templates/protocolAdapters.d.ts +5 -0
  118. package/dist/templates/protocolNormalizers.d.ts +6 -0
  119. package/dist/templates/protocolStepsBuilder.d.ts +2 -0
  120. package/dist/templates/qpcrAdapters.d.ts +5 -0
  121. package/dist/templates/qpcrExpressionCollectionBuilder.d.ts +2 -0
  122. package/dist/templates/qpcrPlateBuilder.d.ts +2 -0
  123. package/dist/templates/reagentAdapters.d.ts +5 -0
  124. package/dist/templates/reagentListBuilder.d.ts +2 -0
  125. package/dist/templates/runNormalizers.d.ts +10 -0
  126. package/dist/templates/sampleNormalizers.d.ts +4 -0
  127. package/dist/templates/samplePrepAdapters.d.ts +4 -0
  128. package/dist/templates/samplePrepBuilder.d.ts +2 -0
  129. package/dist/templates/sampleSheetAdapters.d.ts +5 -0
  130. package/dist/templates/sampleSheetBuilder.d.ts +2 -0
  131. package/dist/templates/targetedMetabolomicsCollectionBuilder.d.ts +2 -0
  132. package/dist/templates/targetedMetabolomicsHelpers.d.ts +5 -0
  133. package/dist/templates/templateControlSchemas.d.ts +400 -0
  134. package/dist/templates/templateEnvelopes.d.ts +9 -0
  135. package/dist/templates/templatePackCollectionBuilder.d.ts +2 -0
  136. package/dist/templates/templatePresetCollectionBuilder.d.ts +18 -0
  137. package/dist/templates/templateValidators.d.ts +13 -0
  138. package/dist/templates/timeCourseAdapters.d.ts +5 -0
  139. package/dist/templates/timeCourseBuilder.d.ts +2 -0
  140. package/dist/templates/types.d.ts +16 -1
  141. package/dist/templates/wellPlateScreenCollectionBuilder.d.ts +2 -0
  142. package/dist/templates/westernBlotAssayCollectionBuilder.d.ts +2 -0
  143. package/dist/{templates-BSlxwV2c.js → templates-bUAWMn5L.js} +3610 -3154
  144. package/dist/templates-bUAWMn5L.js.map +1 -0
  145. package/dist/types/auth.d.ts +2 -0
  146. package/dist/types/components.d.ts +32 -3
  147. package/dist/types/form-builder.d.ts +2 -1
  148. package/dist/types/index.d.ts +4 -1
  149. package/dist/types/instrument.d.ts +56 -0
  150. package/dist/types/platform.d.ts +3 -0
  151. package/dist/{useExperimentData-BbbdI5xT.js → useProtocolTemplates-QZtHFFH2.js} +534 -359
  152. package/dist/useProtocolTemplates-QZtHFFH2.js.map +1 -0
  153. package/dist/utils/rack.d.ts +47 -0
  154. package/package.json +1 -1
  155. package/src/__tests__/components/AppTopBar.test.ts +15 -0
  156. package/src/__tests__/components/BaseTabs.test.ts +15 -0
  157. package/src/__tests__/components/LcmsSequenceTable.test.ts +57 -0
  158. package/src/__tests__/components/ProgressBar.test.ts +18 -0
  159. package/src/__tests__/components/RackEditor.test.ts +125 -0
  160. package/src/__tests__/components/SampleSelector.test.ts +25 -0
  161. package/src/__tests__/components/SegmentedControl.test.ts +45 -0
  162. package/src/__tests__/components/SequenceProgressBar.test.ts +39 -0
  163. package/src/__tests__/components/SettingsModal.test.ts +83 -2
  164. package/src/__tests__/composables/useControlSchema.test.ts +4 -0
  165. package/src/__tests__/composables/useExperimentData.test.ts +23 -0
  166. package/src/__tests__/composables/useExperimentSamples.test.ts +91 -0
  167. package/src/__tests__/templates/templates.test.ts +86 -0
  168. package/src/__tests__/utils/instrument.test.ts +47 -0
  169. package/src/__tests__/utils/lcms.test.ts +73 -0
  170. package/src/__tests__/utils/permissions.test.ts +50 -0
  171. package/src/__tests__/utils/rack.test.ts +120 -0
  172. package/src/components/AppTopBar.vue +1 -0
  173. package/src/components/BaseTabs.vue +22 -1
  174. package/src/components/InstrumentAlertLog.vue +191 -0
  175. package/src/components/InstrumentStateBadge.vue +50 -0
  176. package/src/components/InstrumentStatusCard.vue +188 -0
  177. package/src/components/LcmsSequenceTable.vue +191 -0
  178. package/src/components/ProgressBar.vue +3 -0
  179. package/src/components/RackEditor.vue +73 -2
  180. package/src/components/SampleSelector.vue +28 -9
  181. package/src/components/SegmentedControl.story.vue +17 -0
  182. package/src/components/SegmentedControl.vue +14 -3
  183. package/src/components/SequenceProgressBar.vue +71 -0
  184. package/src/components/SettingsModal.vue +42 -2
  185. package/src/components/WellPlate.vue +142 -21
  186. package/src/components/index.ts +5 -0
  187. package/src/components/internal/WellEditPopupInternal.vue +1 -0
  188. package/src/composables/controlComponentBindings.ts +80 -0
  189. package/src/composables/controlSchemaAdapters.ts +196 -0
  190. package/src/composables/controlSchemaFormFields.ts +61 -0
  191. package/src/composables/controlSchemaLayout.ts +59 -0
  192. package/src/composables/controlSchemaNormalize.ts +101 -0
  193. package/src/composables/controlSchemaUtils.ts +36 -0
  194. package/src/composables/controlWorkspaceOptions.ts +21 -0
  195. package/src/composables/experimentDesignData.ts +182 -0
  196. package/src/composables/index.ts +14 -0
  197. package/src/composables/useAuth.ts +4 -0
  198. package/src/composables/useAutoGroup.ts +5 -1
  199. package/src/composables/useControlSchema.ts +62 -618
  200. package/src/composables/useControlWorkspace.ts +207 -0
  201. package/src/composables/useExperimentData.ts +57 -16
  202. package/src/composables/useExperimentSamples.ts +142 -0
  203. package/src/index.ts +27 -0
  204. package/src/instrument.ts +90 -0
  205. package/src/lcms.ts +108 -0
  206. package/src/permissions.ts +143 -0
  207. package/src/stores/auth.ts +31 -3
  208. package/src/styles/components/instrument-monitor.css +478 -0
  209. package/src/styles/components/lcms-sequence-table.css +189 -0
  210. package/src/styles/components/sequence-progress-bar.css +63 -0
  211. package/src/styles/components/tabs.css +9 -0
  212. package/src/styles/components/well-edit-popup.css +7 -1
  213. package/src/styles/components/well-plate.css +5 -0
  214. package/src/styles/index.css +3 -0
  215. package/src/templates/adapters.ts +89 -930
  216. package/src/templates/assayLookups.ts +33 -0
  217. package/src/templates/assayMatrixAdapters.ts +78 -0
  218. package/src/templates/assayMatrixBuilder.ts +59 -0
  219. package/src/templates/assayNormalizers.ts +34 -0
  220. package/src/templates/builderDefaults.ts +11 -0
  221. package/src/templates/builderIdUtils.ts +20 -0
  222. package/src/templates/builderPresetControls.ts +165 -0
  223. package/src/templates/builderReadUtils.ts +57 -0
  224. package/src/templates/builders.ts +122 -2149
  225. package/src/templates/calibrationCurveAdapters.ts +59 -0
  226. package/src/templates/calibrationCurveBuilder.ts +99 -0
  227. package/src/templates/calibrationNormalizers.ts +60 -0
  228. package/src/templates/componentBindingCatalog.ts +90 -0
  229. package/src/templates/componentBindingHelpers.ts +93 -0
  230. package/src/templates/componentBindings.ts +12 -461
  231. package/src/templates/componentDoseResponseProps.ts +42 -0
  232. package/src/templates/componentGenericProps.ts +77 -0
  233. package/src/templates/componentPlateHelpers.ts +29 -0
  234. package/src/templates/componentPlateMapProps.ts +32 -0
  235. package/src/templates/componentPropsFactory.ts +21 -0
  236. package/src/templates/componentQpcrPlateProps.ts +28 -0
  237. package/src/templates/componentTargetResolvers.ts +69 -0
  238. package/src/templates/componentTemplateProps.ts +78 -0
  239. package/src/templates/controlSchemaClone.ts +32 -0
  240. package/src/templates/controlSchemaConstants.ts +11 -0
  241. package/src/templates/controlSchemaMerge.ts +40 -0
  242. package/src/templates/controlSchemaTargets.ts +87 -0
  243. package/src/templates/controlSchemaTypes.ts +20 -0
  244. package/src/templates/controlSchemas.ts +22 -636
  245. package/src/templates/defaultBioTemplateBuilder.ts +124 -0
  246. package/src/templates/doseResponseAdapters.ts +45 -0
  247. package/src/templates/doseResponseBuilder.ts +44 -0
  248. package/src/templates/elisaAssayCollectionBuilder.ts +62 -0
  249. package/src/templates/flowCytometryAssayCollectionBuilder.ts +41 -0
  250. package/src/templates/flowCytometryPanelBuilder.ts +53 -0
  251. package/src/templates/flowNormalizers.ts +58 -0
  252. package/src/templates/flowPanelAdapters.ts +58 -0
  253. package/src/templates/index.ts +2 -0
  254. package/src/templates/instrumentRunAdapterHelpers.ts +94 -0
  255. package/src/templates/instrumentRunAdapters.ts +163 -0
  256. package/src/templates/instrumentRunBuilder.ts +97 -0
  257. package/src/templates/lcmsBatchCollectionBuilder.ts +38 -0
  258. package/src/templates/plateGeometry.ts +62 -0
  259. package/src/templates/plateMapAdapters.ts +36 -0
  260. package/src/templates/plateMapBuilder.ts +43 -0
  261. package/src/templates/presetControlSchemas.ts +258 -0
  262. package/src/templates/presets.ts +23 -0
  263. package/src/templates/protocolAdapters.ts +69 -0
  264. package/src/templates/protocolNormalizers.ts +37 -0
  265. package/src/templates/protocolStepsBuilder.ts +36 -0
  266. package/src/templates/qpcrAdapters.ts +104 -0
  267. package/src/templates/qpcrExpressionCollectionBuilder.ts +33 -0
  268. package/src/templates/qpcrPlateBuilder.ts +96 -0
  269. package/src/templates/reagentAdapters.ts +77 -0
  270. package/src/templates/reagentListBuilder.ts +30 -0
  271. package/src/templates/runNormalizers.ts +63 -0
  272. package/src/templates/sampleNormalizers.ts +58 -0
  273. package/src/templates/samplePrepAdapters.ts +63 -0
  274. package/src/templates/samplePrepBuilder.ts +51 -0
  275. package/src/templates/sampleSheetAdapters.ts +75 -0
  276. package/src/templates/sampleSheetBuilder.ts +23 -0
  277. package/src/templates/targetedMetabolomicsCollectionBuilder.ts +79 -0
  278. package/src/templates/targetedMetabolomicsHelpers.ts +102 -0
  279. package/src/templates/templateControlSchemas.ts +320 -0
  280. package/src/templates/templateEnvelopes.ts +137 -0
  281. package/src/templates/templatePackCollectionBuilder.ts +23 -0
  282. package/src/templates/templatePresetCollectionBuilder.ts +139 -0
  283. package/src/templates/templateValidators.ts +414 -0
  284. package/src/templates/timeCourseAdapters.ts +73 -0
  285. package/src/templates/timeCourseBuilder.ts +64 -0
  286. package/src/templates/types.ts +17 -0
  287. package/src/templates/wellPlateScreenCollectionBuilder.ts +36 -0
  288. package/src/templates/westernBlotAssayCollectionBuilder.ts +68 -0
  289. package/src/types/auth.ts +3 -0
  290. package/src/types/components.ts +45 -3
  291. package/src/types/form-builder.ts +2 -1
  292. package/src/types/index.ts +35 -0
  293. package/src/types/instrument.ts +61 -0
  294. package/src/types/platform.ts +4 -0
  295. package/src/utils/rack.ts +209 -0
  296. package/dist/auth-CBG3bWEc.js.map +0 -1
  297. package/dist/components-5KSfsVqf.js.map +0 -1
  298. package/dist/composables-D4Myb30a.js.map +0 -1
  299. package/dist/templates-BSlxwV2c.js.map +0 -1
  300. package/dist/useExperimentData-BbbdI5xT.js.map +0 -1
@@ -0,0 +1,414 @@
1
+ import type {
2
+ AssayMatrixTemplateData,
3
+ CalibrationCurveTemplateData,
4
+ DoseResponseTemplateData,
5
+ FlowCytometryPanelTemplateData,
6
+ InstrumentRunTemplateData,
7
+ PlateMapTemplateData,
8
+ ProtocolStepsTemplateData,
9
+ QpcrPlateTemplateData,
10
+ ReagentListTemplateData,
11
+ SamplePrepTemplateData,
12
+ SampleSheetTemplateData,
13
+ TimeCourseTemplateData,
14
+ } from './types'
15
+ import { assertWellInFormat } from './plateGeometry'
16
+
17
+ export function validatePlateMapData(data: PlateMapTemplateData): void {
18
+ if (!Array.isArray(data.plates) || data.plates.length === 0) {
19
+ throw new Error('Plate-map template requires at least one plate.')
20
+ }
21
+ const sampleIds = new Set(data.samples.map(sample => sample.id))
22
+ if (sampleIds.size !== data.samples.length) {
23
+ throw new Error('Plate-map sample ids must be unique.')
24
+ }
25
+ for (const plate of data.plates) {
26
+ for (const [wellId, well] of Object.entries(plate.wells)) {
27
+ if (well.id !== wellId) {
28
+ throw new Error(`Well key '${wellId}' does not match well id '${well.id}'.`)
29
+ }
30
+ if (well.sampleType && !sampleIds.has(well.sampleType)) {
31
+ throw new Error(`Well '${well.id}' references unknown sample '${well.sampleType}'.`)
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ export function validateSampleSheetData(data: SampleSheetTemplateData): void {
38
+ if (!Array.isArray(data.samples) || data.samples.length === 0) {
39
+ throw new Error('Sample-sheet template requires at least one sample.')
40
+ }
41
+ const sampleIds = new Set(data.samples.map(sample => sample.sampleId))
42
+ if (sampleIds.size !== data.samples.length) {
43
+ throw new Error('Sample-sheet sample ids must be unique.')
44
+ }
45
+ for (const group of data.groups) {
46
+ for (const sampleId of group.samples) {
47
+ if (!sampleIds.has(sampleId)) {
48
+ throw new Error(`Group '${group.name}' references unknown sample '${sampleId}'.`)
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ export function validateSamplePrepData(data: SamplePrepTemplateData): void {
55
+ if (!data.protocolName) {
56
+ throw new Error('Sample-prep template requires protocolName.')
57
+ }
58
+ if (!Array.isArray(data.steps) || data.steps.length === 0) {
59
+ throw new Error('Sample-prep template requires at least one step.')
60
+ }
61
+ const stepIds = new Set(data.steps.map(step => step.id))
62
+ if (stepIds.size !== data.steps.length) {
63
+ throw new Error('Sample-prep step ids must be unique.')
64
+ }
65
+ const orders = new Set(data.steps.map(step => step.order))
66
+ if (orders.size !== data.steps.length) {
67
+ throw new Error('Sample-prep step orders must be unique.')
68
+ }
69
+ for (const step of data.steps) {
70
+ if (!step.id || !step.name) {
71
+ throw new Error('Sample-prep steps require id and name.')
72
+ }
73
+ if (step.order < 0) {
74
+ throw new Error(`Sample-prep step '${step.id}' has a negative order.`)
75
+ }
76
+ if (!step.volumeUnit) {
77
+ throw new Error(`Sample-prep step '${step.id}' requires volumeUnit.`)
78
+ }
79
+ if (step.inputVolume !== undefined && step.inputVolume < 0) {
80
+ throw new Error(`Sample-prep step '${step.id}' has a negative input volume.`)
81
+ }
82
+ if (step.outputVolume !== undefined && step.outputVolume < 0) {
83
+ throw new Error(`Sample-prep step '${step.id}' has a negative output volume.`)
84
+ }
85
+ if (step.durationMin !== undefined && step.durationMin < 0) {
86
+ throw new Error(`Sample-prep step '${step.id}' has a negative duration.`)
87
+ }
88
+ if (!step.sourceSampleId && !step.destinationSampleId && !step.reagentId) {
89
+ throw new Error(`Sample-prep step '${step.id}' requires a source, destination, or reagent.`)
90
+ }
91
+ }
92
+ }
93
+
94
+ export function validateDoseResponseData(data: DoseResponseTemplateData): void {
95
+ if (!Array.isArray(data.compounds) || data.compounds.length === 0) {
96
+ throw new Error('Dose-response template requires at least one compound.')
97
+ }
98
+ if (!data.unit) {
99
+ throw new Error('Dose-response template requires a unit.')
100
+ }
101
+ if (data.replicates < 1) {
102
+ throw new Error('Dose-response replicates must be at least 1.')
103
+ }
104
+ for (const compound of data.compounds) {
105
+ if (compound.concentrations.length === 0) {
106
+ throw new Error(`Compound '${compound.name}' requires at least one concentration.`)
107
+ }
108
+ if (compound.concentrations.some(value => value < 0)) {
109
+ throw new Error(`Compound '${compound.name}' has a negative concentration.`)
110
+ }
111
+ }
112
+ }
113
+
114
+ export function validateCalibrationCurveData(data: CalibrationCurveTemplateData): void {
115
+ if (!data.analyte) {
116
+ throw new Error('Calibration-curve template requires an analyte.')
117
+ }
118
+ if (!data.xUnit) {
119
+ throw new Error('Calibration-curve template requires xUnit.')
120
+ }
121
+ if (!data.responseUnit) {
122
+ throw new Error('Calibration-curve template requires responseUnit.')
123
+ }
124
+ if (!Array.isArray(data.points) || data.points.length === 0) {
125
+ throw new Error('Calibration-curve template requires at least one point.')
126
+ }
127
+ if (data.acceptance.minRSquared < 0 || data.acceptance.minRSquared > 1) {
128
+ throw new Error('Calibration-curve minRSquared must be between 0 and 1.')
129
+ }
130
+ if (data.acceptance.standardTolerancePercent < 0 || data.acceptance.qcTolerancePercent < 0) {
131
+ throw new Error('Calibration-curve tolerances cannot be negative.')
132
+ }
133
+
134
+ const pointIds = new Set(data.points.map(point => point.id))
135
+ if (pointIds.size !== data.points.length) {
136
+ throw new Error('Calibration-curve point ids must be unique.')
137
+ }
138
+ const orders = new Set(data.points.map(point => point.order))
139
+ if (orders.size !== data.points.length) {
140
+ throw new Error('Calibration-curve point orders must be unique.')
141
+ }
142
+
143
+ let standards = 0
144
+ for (const point of data.points) {
145
+ if (!point.id || !point.level) {
146
+ throw new Error('Calibration-curve points require id and level.')
147
+ }
148
+ if (!point.unit) {
149
+ throw new Error(`Calibration-curve point '${point.id}' requires unit.`)
150
+ }
151
+ if (point.order < 0) {
152
+ throw new Error(`Calibration-curve point '${point.id}' has a negative order.`)
153
+ }
154
+ if (point.concentration < 0) {
155
+ throw new Error(`Calibration-curve point '${point.id}' has a negative concentration.`)
156
+ }
157
+ if ((point.role === 'standard' || point.role === 'qc') && point.concentration <= 0) {
158
+ throw new Error(`Calibration-curve point '${point.id}' requires positive concentration.`)
159
+ }
160
+ if (point.replicate < 1) {
161
+ throw new Error(`Calibration-curve point '${point.id}' replicate must be at least 1.`)
162
+ }
163
+ if (point.role === 'standard' && point.include) {
164
+ standards += 1
165
+ }
166
+ }
167
+
168
+ if (standards < 2) {
169
+ throw new Error('Calibration-curve template requires at least two included standards.')
170
+ }
171
+ if (data.acceptance.requireBlank && !data.points.some(point => point.role === 'blank')) {
172
+ throw new Error('Calibration-curve template requires a blank point when requireBlank is true.')
173
+ }
174
+ }
175
+
176
+ export function validateTimeCourseData(data: TimeCourseTemplateData): void {
177
+ if (!Array.isArray(data.timepoints) || data.timepoints.length === 0) {
178
+ throw new Error('Time-course template requires at least one time point.')
179
+ }
180
+ if (!Array.isArray(data.conditions) || data.conditions.length === 0) {
181
+ throw new Error('Time-course template requires at least one condition.')
182
+ }
183
+ const timepointIds = new Set(data.timepoints.map(timepoint => timepoint.id))
184
+ if (timepointIds.size !== data.timepoints.length) {
185
+ throw new Error('Time-course time point ids must be unique.')
186
+ }
187
+ const conditionIds = new Set(data.conditions.map(condition => condition.id))
188
+ if (conditionIds.size !== data.conditions.length) {
189
+ throw new Error('Time-course condition ids must be unique.')
190
+ }
191
+ const sampleIds = new Set(data.samples.map(sample => sample.sampleId))
192
+ if (sampleIds.size !== data.samples.length) {
193
+ throw new Error('Time-course sample ids must be unique.')
194
+ }
195
+ for (const sample of data.samples) {
196
+ if (!timepointIds.has(sample.timepointId)) {
197
+ throw new Error(`Sample '${sample.sampleId}' references unknown time point.`)
198
+ }
199
+ if (!conditionIds.has(sample.conditionId)) {
200
+ throw new Error(`Sample '${sample.sampleId}' references unknown condition.`)
201
+ }
202
+ }
203
+ }
204
+
205
+ export function validateProtocolStepsData(data: ProtocolStepsTemplateData): void {
206
+ if (!Array.isArray(data.steps) || data.steps.length === 0) {
207
+ throw new Error('Protocol-steps template requires at least one step.')
208
+ }
209
+ const stepIds = new Set(data.steps.map(step => step.id))
210
+ if (stepIds.size !== data.steps.length) {
211
+ throw new Error('Protocol-steps ids must be unique.')
212
+ }
213
+ const orders = new Set(data.steps.map(step => step.order))
214
+ if (orders.size !== data.steps.length) {
215
+ throw new Error('Protocol-steps orders must be unique.')
216
+ }
217
+ for (const step of data.steps) {
218
+ if (!step.id) {
219
+ throw new Error('Protocol step id is required.')
220
+ }
221
+ if (!step.name) {
222
+ throw new Error(`Protocol step '${step.id}' requires a name.`)
223
+ }
224
+ if (step.duration !== undefined && step.duration < 0) {
225
+ throw new Error(`Protocol step '${step.id}' has a negative duration.`)
226
+ }
227
+ if (step.order < 0) {
228
+ throw new Error(`Protocol step '${step.id}' has a negative order.`)
229
+ }
230
+ }
231
+ }
232
+
233
+ export function validateAssayMatrixData(data: AssayMatrixTemplateData): void {
234
+ if (!Array.isArray(data.samples) || data.samples.length === 0) {
235
+ throw new Error('Assay-matrix template requires at least one sample.')
236
+ }
237
+ if (!Array.isArray(data.features) || data.features.length === 0) {
238
+ throw new Error('Assay-matrix template requires at least one feature.')
239
+ }
240
+ const sampleIds = new Set(data.samples.map(sample => sample.sampleId))
241
+ if (sampleIds.size !== data.samples.length) {
242
+ throw new Error('Assay-matrix sample ids must be unique.')
243
+ }
244
+ const featureIds = new Set(data.features.map(feature => feature.id))
245
+ if (featureIds.size !== data.features.length) {
246
+ throw new Error('Assay-matrix feature ids must be unique.')
247
+ }
248
+ const measurementIds = new Set<string>()
249
+ for (const measurement of data.measurements) {
250
+ const key = `${measurement.sampleId}:${measurement.featureId}`
251
+ if (measurementIds.has(key)) {
252
+ throw new Error(`Duplicate assay measurement for '${key}'.`)
253
+ }
254
+ measurementIds.add(key)
255
+ if (!sampleIds.has(measurement.sampleId)) {
256
+ throw new Error(`Measurement references unknown sample '${measurement.sampleId}'.`)
257
+ }
258
+ if (!featureIds.has(measurement.featureId)) {
259
+ throw new Error(`Measurement references unknown feature '${measurement.featureId}'.`)
260
+ }
261
+ }
262
+ }
263
+
264
+ export function validateReagentListData(data: ReagentListTemplateData): void {
265
+ if (!Array.isArray(data.reagents) || data.reagents.length === 0) {
266
+ throw new Error('Reagent-list template requires at least one reagent.')
267
+ }
268
+ const reagentIds = new Set(data.reagents.map(reagent => reagent.id))
269
+ if (reagentIds.size !== data.reagents.length) {
270
+ throw new Error('Reagent-list reagent ids must be unique.')
271
+ }
272
+ for (const reagent of data.reagents) {
273
+ if (!reagent.id) {
274
+ throw new Error('Reagent-list reagent id is required.')
275
+ }
276
+ if (!reagent.name) {
277
+ throw new Error(`Reagent '${reagent.id}' requires a name.`)
278
+ }
279
+ if (reagent.stockLevel !== undefined && reagent.stockLevel < 0) {
280
+ throw new Error(`Reagent '${reagent.id}' has a negative stock level.`)
281
+ }
282
+ }
283
+ }
284
+
285
+ export function validateFlowCytometryPanelData(data: FlowCytometryPanelTemplateData): void {
286
+ if (!Array.isArray(data.markers) || data.markers.length === 0) {
287
+ throw new Error('Flow-cytometry panel template requires at least one marker.')
288
+ }
289
+ const markerIds = new Set(data.markers.map(marker => marker.id))
290
+ if (markerIds.size !== data.markers.length) {
291
+ throw new Error('Flow-cytometry panel marker ids must be unique.')
292
+ }
293
+ const controlIds = new Set(data.controls.map(control => control.id))
294
+ if (controlIds.size !== data.controls.length) {
295
+ throw new Error('Flow-cytometry panel control ids must be unique.')
296
+ }
297
+ for (const marker of data.markers) {
298
+ if (!marker.id) {
299
+ throw new Error('Flow-cytometry panel marker id is required.')
300
+ }
301
+ if (!marker.marker) {
302
+ throw new Error(`Flow-cytometry panel marker '${marker.id}' requires a marker name.`)
303
+ }
304
+ }
305
+ for (const control of data.controls) {
306
+ if (!control.id) {
307
+ throw new Error('Flow-cytometry panel control id is required.')
308
+ }
309
+ if (!control.name) {
310
+ throw new Error(`Flow-cytometry panel control '${control.id}' requires a name.`)
311
+ }
312
+ if (control.markerId && !markerIds.has(control.markerId)) {
313
+ throw new Error(`Flow-cytometry panel control '${control.id}' references unknown marker '${control.markerId}'.`)
314
+ }
315
+ }
316
+ }
317
+
318
+ export function validateInstrumentRunData(data: InstrumentRunTemplateData): void {
319
+ if (!Array.isArray(data.methods) || data.methods.length === 0) {
320
+ throw new Error('Instrument-run template requires at least one method.')
321
+ }
322
+ if (!Array.isArray(data.items) || data.items.length === 0) {
323
+ throw new Error('Instrument-run template requires at least one run item.')
324
+ }
325
+ const methodIds = new Set(data.methods.map(method => method.id))
326
+ if (methodIds.size !== data.methods.length) {
327
+ throw new Error('Instrument-run method ids must be unique.')
328
+ }
329
+ for (const method of data.methods) {
330
+ if (!method.id || !method.name) {
331
+ throw new Error('Instrument-run methods require id and name.')
332
+ }
333
+ }
334
+ const itemIds = new Set(data.items.map(item => item.id))
335
+ if (itemIds.size !== data.items.length) {
336
+ throw new Error('Instrument-run item ids must be unique.')
337
+ }
338
+ const orders = new Set(data.items.map(item => item.order))
339
+ if (orders.size !== data.items.length) {
340
+ throw new Error('Instrument-run item orders must be unique.')
341
+ }
342
+ for (const item of data.items) {
343
+ if (!item.id) {
344
+ throw new Error('Instrument-run items require id.')
345
+ }
346
+ if (!methodIds.has(item.methodId)) {
347
+ throw new Error(`Instrument-run item '${item.id}' references unknown method '${item.methodId}'.`)
348
+ }
349
+ if (item.kind === 'sample' && !item.sampleId) {
350
+ throw new Error(`Instrument-run sample item '${item.id}' requires sampleId.`)
351
+ }
352
+ if (item.order < 0) {
353
+ throw new Error(`Instrument-run item '${item.id}' has a negative order.`)
354
+ }
355
+ if (item.injectionVolume !== undefined && item.injectionVolume < 0) {
356
+ throw new Error(`Instrument-run item '${item.id}' has a negative injection volume.`)
357
+ }
358
+ if (item.expectedDurationMin !== undefined && item.expectedDurationMin < 0) {
359
+ throw new Error(`Instrument-run item '${item.id}' has a negative expected duration.`)
360
+ }
361
+ }
362
+ }
363
+
364
+ export function validateQpcrPlateData(data: QpcrPlateTemplateData): void {
365
+ if (!Array.isArray(data.samples) || data.samples.length === 0) {
366
+ throw new Error('qPCR plate template requires at least one sample.')
367
+ }
368
+ if (!Array.isArray(data.targets) || data.targets.length === 0) {
369
+ throw new Error('qPCR plate template requires at least one target.')
370
+ }
371
+ if (!Array.isArray(data.reactions) || data.reactions.length === 0) {
372
+ throw new Error('qPCR plate template requires at least one reaction.')
373
+ }
374
+ const sampleIds = new Set(data.samples.map(sample => sample.sampleId))
375
+ if (sampleIds.size !== data.samples.length) {
376
+ throw new Error('qPCR plate sample ids must be unique.')
377
+ }
378
+ const targetIds = new Set(data.targets.map(target => target.id))
379
+ if (targetIds.size !== data.targets.length) {
380
+ throw new Error('qPCR plate target ids must be unique.')
381
+ }
382
+ const reactionIds = new Set(data.reactions.map(reaction => reaction.id))
383
+ if (reactionIds.size !== data.reactions.length) {
384
+ throw new Error('qPCR plate reaction ids must be unique.')
385
+ }
386
+ const wellIds = new Set(data.reactions.map(reaction => reaction.wellId))
387
+ if (wellIds.size !== data.reactions.length) {
388
+ throw new Error('qPCR plate reaction wells must be unique.')
389
+ }
390
+ for (const reaction of data.reactions) {
391
+ assertWellInFormat(reaction.wellId, data.format)
392
+ if (!reaction.id) {
393
+ throw new Error('qPCR reaction id is required.')
394
+ }
395
+ if (!reaction.targetId || !targetIds.has(reaction.targetId)) {
396
+ throw new Error(`qPCR reaction '${reaction.id}' references unknown target '${reaction.targetId}'.`)
397
+ }
398
+ if (reaction.sampleId && !sampleIds.has(reaction.sampleId)) {
399
+ throw new Error(`qPCR reaction '${reaction.id}' references unknown sample '${reaction.sampleId}'.`)
400
+ }
401
+ if (reaction.controlKind === 'sample' && !reaction.sampleId) {
402
+ throw new Error(`qPCR sample reaction '${reaction.id}' requires sampleId.`)
403
+ }
404
+ if (reaction.replicate < 1) {
405
+ throw new Error(`qPCR reaction '${reaction.id}' replicate must be at least 1.`)
406
+ }
407
+ if (reaction.cq !== undefined && reaction.cq !== null && reaction.cq < 0) {
408
+ throw new Error(`qPCR reaction '${reaction.id}' has a negative Cq.`)
409
+ }
410
+ if (reaction.quantity !== undefined && reaction.quantity !== null && reaction.quantity < 0) {
411
+ throw new Error(`qPCR reaction '${reaction.id}' has a negative quantity.`)
412
+ }
413
+ }
414
+ }
@@ -0,0 +1,73 @@
1
+ import { getTemplateData } from './templateEnvelopes'
2
+ import { validateTimeCourseData } from './templateValidators'
3
+ import type {
4
+ TimeCourseColumnsAdapterResult,
5
+ TimeCourseDataFrameAdapterResult,
6
+ TimeCourseRowsAdapterResult,
7
+ TimeCourseStepsAdapterResult,
8
+ TimeCourseTemplate,
9
+ TimeCourseTemplateData,
10
+ } from './types'
11
+
12
+ export function toTimeCourseRows(
13
+ template: TimeCourseTemplate | TimeCourseTemplateData
14
+ ): TimeCourseRowsAdapterResult {
15
+ const data = getTemplateData(template, 'time-course')
16
+ validateTimeCourseData(data)
17
+ const timepoints = new Map(data.timepoints.map(timepoint => [timepoint.id, timepoint]))
18
+ const conditions = new Map(data.conditions.map(condition => [condition.id, condition]))
19
+
20
+ return data.samples.map(sample => {
21
+ const timepoint = timepoints.get(sample.timepointId)
22
+ const condition = conditions.get(sample.conditionId)
23
+ return {
24
+ sampleId: sample.sampleId,
25
+ condition: condition?.name ?? sample.conditionId,
26
+ timepoint: timepoint?.label ?? sample.timepointId,
27
+ value: timepoint?.value,
28
+ unit: timepoint?.unit,
29
+ replicate: sample.replicate,
30
+ ...(sample.metadata ?? {}),
31
+ }
32
+ })
33
+ }
34
+
35
+ export function toTimeCourseColumns(): TimeCourseColumnsAdapterResult {
36
+ return [
37
+ { key: 'sampleId', label: 'Sample ID', sortable: true },
38
+ { key: 'condition', label: 'Condition', sortable: true },
39
+ { key: 'timepoint', label: 'Time Point', sortable: true },
40
+ { key: 'value', label: 'Value', sortable: true, align: 'right' },
41
+ { key: 'unit', label: 'Unit', sortable: true },
42
+ { key: 'replicate', label: 'Replicate', sortable: true, align: 'right' },
43
+ ]
44
+ }
45
+
46
+ export function toTimeCourseDataFrame(
47
+ template: TimeCourseTemplate | TimeCourseTemplateData
48
+ ): TimeCourseDataFrameAdapterResult {
49
+ return {
50
+ data: toTimeCourseRows(template),
51
+ columns: toTimeCourseColumns(),
52
+ rowKey: 'sampleId',
53
+ }
54
+ }
55
+
56
+ export function toTimeCourseSteps(
57
+ template: TimeCourseTemplate | TimeCourseTemplateData
58
+ ): TimeCourseStepsAdapterResult {
59
+ const data = getTemplateData(template, 'time-course')
60
+ validateTimeCourseData(data)
61
+ return data.timepoints.map((timepoint, index) => ({
62
+ id: timepoint.id,
63
+ type: 'measurement',
64
+ name: timepoint.label,
65
+ description: `${data.conditions.length} condition${data.conditions.length === 1 ? '' : 's'}`,
66
+ status: 'pending',
67
+ order: index,
68
+ parameters: {
69
+ value: timepoint.value,
70
+ unit: timepoint.unit,
71
+ },
72
+ }))
73
+ }
@@ -0,0 +1,64 @@
1
+ import { DEFAULT_SAMPLE_COLORS } from './builderDefaults'
2
+ import {
3
+ idFromName,
4
+ timepointId,
5
+ } from './builderIdUtils'
6
+ import {
7
+ normalizeCondition,
8
+ normalizeTimePoint,
9
+ } from './protocolNormalizers'
10
+ import { createTemplateEnvelope } from './templateEnvelopes'
11
+ import { validateTimeCourseData } from './templateValidators'
12
+ import type {
13
+ CreateTimeCourseTemplateOptions,
14
+ TimeCourseCondition,
15
+ TimeCourseTemplate,
16
+ TimeCourseTemplateData,
17
+ TimePoint,
18
+ } from './types'
19
+
20
+ export function createTimeCourseTemplate(
21
+ options: CreateTimeCourseTemplateOptions
22
+ ): TimeCourseTemplate {
23
+ const timepoints = options.timepoints.map((timepoint, index): TimePoint => {
24
+ if (typeof timepoint !== 'number') return normalizeTimePoint(timepoint)
25
+ return {
26
+ id: timepointId(timepoint, options.unit, index),
27
+ label: `${timepoint} ${options.unit}`,
28
+ value: timepoint,
29
+ unit: options.unit,
30
+ metadata: {},
31
+ }
32
+ }).sort((a, b) => a.value - b.value)
33
+
34
+ const conditions = options.conditions.map((condition, index): TimeCourseCondition => {
35
+ if (typeof condition !== 'string') return normalizeCondition(condition)
36
+ return {
37
+ id: idFromName(condition, `condition-${index + 1}`),
38
+ name: condition,
39
+ color: DEFAULT_SAMPLE_COLORS[index % DEFAULT_SAMPLE_COLORS.length],
40
+ metadata: {},
41
+ }
42
+ })
43
+
44
+ const samples = conditions.flatMap(condition =>
45
+ timepoints.flatMap(timepoint =>
46
+ Array.from({ length: options.replicates ?? 1 }, (_, index) => ({
47
+ sampleId: `${condition.id}-${timepoint.id}-r${index + 1}`,
48
+ conditionId: condition.id,
49
+ timepointId: timepoint.id,
50
+ replicate: index + 1,
51
+ metadata: {},
52
+ }))
53
+ )
54
+ )
55
+
56
+ const data: TimeCourseTemplateData = {
57
+ timepoints,
58
+ conditions,
59
+ samples,
60
+ metadata: options.metadata ?? {},
61
+ }
62
+ validateTimeCourseData(data)
63
+ return createTemplateEnvelope('time-course', data, options.metadata ?? {})
64
+ }
@@ -31,6 +31,7 @@ export type TemplatePresetId =
31
31
  | 'wellplate-screen'
32
32
  | 'qpcr-expression'
33
33
  | 'lcms-batch'
34
+ | 'targeted-metabolomics'
34
35
  | 'elisa-assay'
35
36
  | 'flow-cytometry-assay'
36
37
  | 'western-blot-assay'
@@ -596,6 +597,22 @@ export interface CreateLcmsBatchCollectionOptions {
596
597
  metadata?: Record<string, unknown>
597
598
  }
598
599
 
600
+ export interface CreateTargetedMetabolomicsCollectionOptions {
601
+ samples?: Array<string | SampleRecord>
602
+ metabolites?: Array<string | AssayFeature>
603
+ internalStandards?: Array<string | ReagentTemplateInput>
604
+ standardConcentrations?: number[]
605
+ concentrationUnit?: string
606
+ responseUnit?: string
607
+ calibrationAnalyte?: string
608
+ instrument?: string
609
+ method?: string | InstrumentMethodInput
610
+ prepType?: SamplePrepStepType
611
+ outputVolume?: number
612
+ includeQc?: boolean
613
+ metadata?: Record<string, unknown>
614
+ }
615
+
599
616
  export interface CreateElisaAssayCollectionOptions {
600
617
  samples?: Array<string | SampleRecord>
601
618
  analyte?: string
@@ -0,0 +1,36 @@
1
+ import { DEFAULT_SAMPLE_COLORS } from './builderDefaults'
2
+ import { createDoseResponseTemplate } from './doseResponseBuilder'
3
+ import { createPlateMapTemplate } from './plateMapBuilder'
4
+ import { createSampleSheetTemplate } from './sampleSheetBuilder'
5
+ import { presetSampleRecords } from './sampleNormalizers'
6
+ import { createTemplateCollection } from './templateEnvelopes'
7
+ import type {
8
+ CreateWellPlateScreenCollectionOptions,
9
+ TemplateCollectionEnvelope,
10
+ } from './types'
11
+
12
+ export function createWellPlateScreenCollection(
13
+ options: CreateWellPlateScreenCollectionOptions = {}
14
+ ): TemplateCollectionEnvelope {
15
+ const sampleRecords = presetSampleRecords(options.samples ?? ['Control', 'Treatment'])
16
+ const plate = createPlateMapTemplate({
17
+ name: options.plateName ?? 'Screen plate',
18
+ format: options.plateFormat ?? 96,
19
+ samples: sampleRecords.map((sample, index) => ({
20
+ id: sample.sampleId,
21
+ name: sample.name ?? sample.sampleId,
22
+ color: DEFAULT_SAMPLE_COLORS[index % DEFAULT_SAMPLE_COLORS.length],
23
+ })),
24
+ })
25
+ const sampleSheet = createSampleSheetTemplate({ samples: sampleRecords })
26
+ const doseResponse = createDoseResponseTemplate({
27
+ compounds: options.compounds ?? { 'Compound A': [10, 1, 0.1] },
28
+ unit: options.unit ?? 'uM',
29
+ replicates: options.replicates ?? 3,
30
+ layout: plate,
31
+ })
32
+ return createTemplateCollection(
33
+ [plate, sampleSheet, doseResponse],
34
+ { preset: 'wellplate-screen', ...(options.metadata ?? {}) }
35
+ )
36
+ }
@@ -0,0 +1,68 @@
1
+ import { createAssayMatrixTemplate } from './assayMatrixBuilder'
2
+ import { idFromName } from './builderIdUtils'
3
+ import { createProtocolStepsTemplate } from './protocolStepsBuilder'
4
+ import { createReagentListTemplate } from './reagentListBuilder'
5
+ import { createSampleSheetTemplate } from './sampleSheetBuilder'
6
+ import { presetSampleRecords } from './sampleNormalizers'
7
+ import { createTemplateCollection } from './templateEnvelopes'
8
+ import type {
9
+ CreateWesternBlotAssayCollectionOptions,
10
+ TemplateCollectionEnvelope,
11
+ } from './types'
12
+
13
+ export function createWesternBlotAssayCollection(
14
+ options: CreateWesternBlotAssayCollectionOptions = {}
15
+ ): TemplateCollectionEnvelope {
16
+ const sampleRecords = presetSampleRecords(options.samples ?? ['Control', 'Treatment'])
17
+ const loadingControl = options.loadingControl ?? 'ACTB'
18
+ const targetNames = (options.targets ?? ['Target protein']).map(target =>
19
+ typeof target === 'string' ? target : target.name ?? target.id
20
+ )
21
+ const featureNames = loadingControl && !targetNames.includes(loadingControl)
22
+ ? [...targetNames, loadingControl]
23
+ : targetNames
24
+ const sampleSheet = createSampleSheetTemplate({ samples: sampleRecords })
25
+ const reagentList = createReagentListTemplate({
26
+ reagents: [
27
+ { id: 'primary-antibody', name: 'Primary antibody', kind: 'antibody', storageCondition: '4C' },
28
+ { id: 'secondary-antibody', name: 'Secondary antibody', kind: 'antibody', storageCondition: '4C' },
29
+ { id: 'lysis-buffer', name: 'Lysis buffer', kind: 'buffer', storageCondition: '4C' },
30
+ { id: 'blocking-buffer', name: 'Blocking buffer', kind: 'buffer', storageCondition: '4C' },
31
+ { id: 'wash-buffer', name: 'TBST wash buffer', kind: 'buffer', storageCondition: 'RT' },
32
+ { id: 'substrate', name: 'Detection substrate', kind: 'reagent', storageCondition: '4C' },
33
+ ],
34
+ metadata: { assay: 'western-blot' },
35
+ })
36
+ const protocol = createProtocolStepsTemplate({
37
+ steps: [
38
+ { id: 'lyse-samples', type: 'addition', name: 'Lyse samples', duration: 30 },
39
+ { id: 'run-gel', type: 'custom', name: 'Run SDS-PAGE', duration: 90 },
40
+ { id: 'transfer', type: 'transfer', name: 'Transfer to membrane', duration: 60 },
41
+ { id: 'block', type: 'incubation', name: 'Block membrane', duration: 60 },
42
+ { id: 'primary-antibody', type: 'incubation', name: 'Primary antibody incubation', duration: 960 },
43
+ { id: 'secondary-antibody', type: 'incubation', name: 'Secondary antibody incubation', duration: 60 },
44
+ { id: 'detect', type: 'measurement', name: 'Detect and quantify bands', duration: 30 },
45
+ ],
46
+ metadata: { assay: 'western-blot' },
47
+ })
48
+ const assayMatrix = createAssayMatrixTemplate({
49
+ samples: sampleRecords.map(sample => ({
50
+ sampleId: sample.sampleId,
51
+ name: sample.name,
52
+ group: sample.group,
53
+ metadata: sample.metadata ?? {},
54
+ })),
55
+ features: featureNames.map((feature, index) => ({
56
+ id: idFromName(feature, `feature-${index + 1}`),
57
+ name: feature,
58
+ type: 'protein',
59
+ unit: 'relative intensity',
60
+ metadata: { loadingControl: feature === loadingControl },
61
+ })),
62
+ metadata: { assay: 'western-blot', loadingControl },
63
+ })
64
+ return createTemplateCollection(
65
+ [sampleSheet, reagentList, protocol, assayMatrix],
66
+ { preset: 'western-blot-assay', ...(options.metadata ?? {}) }
67
+ )
68
+ }