@tailor-platform/sdk 1.73.2 → 1.73.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @tailor-platform/sdk
2
2
 
3
+ ## 1.73.3
4
+ ### Patch Changes
5
+
6
+
7
+
8
+ - [#1632](https://github.com/tailor-platform/sdk/pull/1632) [`56cb49d`](https://github.com/tailor-platform/sdk/commit/56cb49db80fd47f37eb43ea5d5f0c4e093cb6909) Thanks [@dqn](https://github.com/dqn)! - Validate that script expressions generated from TailorDB hooks and validators are valid JavaScript, failing at build time with a clear error instead of deploying broken scripts
9
+
10
+
11
+
12
+ - [#1635](https://github.com/tailor-platform/sdk/pull/1635) [`0af2c23`](https://github.com/tailor-platform/sdk/commit/0af2c2346b34f5c026d0221e151ba35ca9148d9a) Thanks [@dqn](https://github.com/dqn)! - Fix TailorDB relations silently dropping a forward relationship when two fields on the same type default to the same forward name; this now throws a validation error instead, matching the existing behavior for duplicate backward relationship names
13
+
14
+
15
+
16
+ - [#1631](https://github.com/tailor-platform/sdk/pull/1631) [`fffc654`](https://github.com/tailor-platform/sdk/commit/fffc6548935e4329dcd65e8d91e2ae1a76833abf) Thanks [@dqn](https://github.com/dqn)! - Fail with a clear error instead of producing corrupted bundle code when build-time source rewriting would apply overlapping edits
17
+
18
+
19
+
20
+ - [#1630](https://github.com/tailor-platform/sdk/pull/1630) [`69a034e`](https://github.com/tailor-platform/sdk/commit/69a034e36f1e2dbf9dfcf6e9d86a4a615502d24d) Thanks [@dqn](https://github.com/dqn)! - Fix `workflow.trigger()` calls in resolvers, executors, and workflow jobs failing at runtime when called without an options argument, or with options passed as a variable, a spread, or an object without a literal `authInvoker` property. All these forms are now rewritten at build time as the documented `trigger(args, options?)` signature promises; previously they compiled but threw "workflow.trigger() is rewritten at build time and unavailable in the bundle" after deploy.
21
+
3
22
  ## 1.73.2
4
23
  ### Patch Changes
5
24
 
@@ -0,0 +1,3 @@
1
+ import { n as generatePluginFilesIfNeeded, r as loadApplication, t as defineApplication } from "./application-DgTCDMZY.mjs";
2
+
3
+ export { defineApplication, generatePluginFilesIfNeeded };
@@ -2324,6 +2324,32 @@ function getImportSource(node) {
2324
2324
  }
2325
2325
  return null;
2326
2326
  }
2327
+ function argumentSourceText(arg, sourceText) {
2328
+ if (arg && typeof arg === "object" && "start" in arg && "end" in arg) return sourceText.slice(arg.start, arg.end);
2329
+ }
2330
+ /**
2331
+ * Get metadata for a static `identifier.trigger(...)` call.
2332
+ * @param node - AST node to inspect
2333
+ * @param sourceText - Source code text
2334
+ * @returns Trigger call metadata, or null when the node is not a trigger call
2335
+ */
2336
+ function getTriggerCallInfo(node, sourceText) {
2337
+ if (!node || typeof node !== "object" || node.type !== "CallExpression") return null;
2338
+ const callExpr = node;
2339
+ const callee = callExpr.callee;
2340
+ if (callee.type !== "MemberExpression") return null;
2341
+ const memberExpr = callee;
2342
+ if (memberExpr.computed || memberExpr.object.type !== "Identifier" || memberExpr.property.name !== "trigger") return null;
2343
+ return {
2344
+ identifierName: memberExpr.object.name,
2345
+ callRange: {
2346
+ start: callExpr.start,
2347
+ end: callExpr.end
2348
+ },
2349
+ argsText: argumentSourceText(callExpr.arguments[0], sourceText) ?? "",
2350
+ optionsText: argumentSourceText(callExpr.arguments[1], sourceText)
2351
+ };
2352
+ }
2327
2353
  /**
2328
2354
  * Unwrap AwaitExpression to get the inner expression
2329
2355
  * @param node - AST expression node
@@ -2370,12 +2396,20 @@ function findProperty(properties, name) {
2370
2396
  /**
2371
2397
  * Apply string replacements to source code
2372
2398
  * Replacements are applied from end to start to maintain positions
2399
+ * Ranges must not overlap; applying an overlapping range on top of an
2400
+ * already-shifted string would splice at stale offsets and corrupt the output,
2401
+ * so overlap is rejected up front
2373
2402
  * @param source - Original source code
2374
2403
  * @param replacements - Replacements to apply
2375
2404
  * @returns Transformed source code
2376
2405
  */
2377
2406
  function applyReplacements(source, replacements) {
2378
2407
  const sorted = replacements.toSorted((a, b) => b.start - a.start);
2408
+ for (let i = 0; i + 1 < sorted.length; i++) {
2409
+ const current = assertDefined(sorted[i], `replacement missing at index ${i}`);
2410
+ const previous = assertDefined(sorted[i + 1], `replacement missing at index ${i + 1}`);
2411
+ if (previous.end > current.start) throw new Error(`applyReplacements: overlapping replacement ranges [${previous.start}, ${previous.end}) and [${current.start}, ${current.end})`);
2412
+ }
2379
2413
  let result = source;
2380
2414
  for (const r of sorted) result = result.slice(0, r.start) + r.text + result.slice(r.end);
2381
2415
  return result;
@@ -2570,29 +2604,12 @@ function detectTriggerCalls(program, sourceText) {
2570
2604
  const calls = [];
2571
2605
  function walk(node) {
2572
2606
  if (!node || typeof node !== "object") return;
2573
- if (node.type === "CallExpression") {
2574
- const callExpr = node;
2575
- const callee = callExpr.callee;
2576
- if (callee.type === "MemberExpression") {
2577
- const memberExpr = callee;
2578
- if (!memberExpr.computed && memberExpr.object.type === "Identifier" && memberExpr.property.name === "trigger") {
2579
- const identifierName = memberExpr.object.name;
2580
- let argsText = "";
2581
- if (callExpr.arguments.length > 0) {
2582
- const firstArg = callExpr.arguments[0];
2583
- if (firstArg && "start" in firstArg && "end" in firstArg) argsText = sourceText.slice(firstArg.start, firstArg.end);
2584
- }
2585
- calls.push({
2586
- identifierName,
2587
- callRange: {
2588
- start: callExpr.start,
2589
- end: callExpr.end
2590
- },
2591
- argsText
2592
- });
2593
- }
2594
- }
2595
- }
2607
+ const triggerCall = getTriggerCallInfo(node, sourceText);
2608
+ if (triggerCall) calls.push({
2609
+ identifierName: triggerCall.identifierName,
2610
+ callRange: triggerCall.callRange,
2611
+ argsText: triggerCall.argsText
2612
+ });
2596
2613
  for (const key of Object.keys(node)) {
2597
2614
  const child = node[key];
2598
2615
  if (Array.isArray(child)) child.forEach((c) => walk(c));
@@ -2698,44 +2715,19 @@ function detectDefaultImports(program) {
2698
2715
  * Name of the injected runtime normalizer helper. Chosen to be unique enough
2699
2716
  * to avoid collisions with user code.
2700
2717
  */
2701
- const NORMALIZER_IDENTIFIER = "__tailor_normalizeAuthInvoker";
2718
+ const NORMALIZER_IDENTIFIER = "__tailor_normalizeTriggerOptions";
2702
2719
  /**
2703
2720
  * Build the source text of the injected normalizer helper.
2704
2721
  *
2705
- * Accepts either a plain string (machine user name) or the object form
2706
- * `{ namespace, machineUserName }`, and always returns the object form.
2707
- * The auth namespace is baked in at bundle time.
2722
+ * Expands a plain-string `authInvoker` (machine user name) in the trigger
2723
+ * options to the object form `{ namespace, machineUserName }`; any other
2724
+ * options value passes through unchanged. The auth namespace is baked in at
2725
+ * bundle time.
2708
2726
  * @param authNamespace - Auth service namespace to embed
2709
2727
  * @returns Source line defining the helper
2710
2728
  */
2711
2729
  function buildNormalizerHelperSource(authNamespace) {
2712
- return `const ${NORMALIZER_IDENTIFIER} = (v) => typeof v === "string" ? { namespace: ${JSON.stringify(authNamespace)}, machineUserName: v } : v;\n`;
2713
- }
2714
- /**
2715
- * Extract authInvoker info from a config object expression
2716
- * Returns the authInvoker value text and whether it's a shorthand property
2717
- * @param configArg - Config argument node
2718
- * @param sourceText - Source code text
2719
- * @returns Extracted authInvoker info, if any
2720
- */
2721
- function extractAuthInvokerInfo(configArg, sourceText) {
2722
- if (!configArg || typeof configArg !== "object") return void 0;
2723
- if (configArg.type !== "ObjectExpression") return void 0;
2724
- const objExpr = configArg;
2725
- for (const prop of objExpr.properties) {
2726
- if (prop.type !== "Property") continue;
2727
- const objProp = prop;
2728
- if ((objProp.key.type === "Identifier" ? objProp.key.name : objProp.key.type === "Literal" ? objProp.key.value : null) === "authInvoker") {
2729
- if (objProp.shorthand) return {
2730
- isShorthand: true,
2731
- valueText: "authInvoker"
2732
- };
2733
- return {
2734
- isShorthand: false,
2735
- valueText: sourceText.slice(objProp.value.start, objProp.value.end)
2736
- };
2737
- }
2738
- }
2730
+ return `const ${NORMALIZER_IDENTIFIER} = (o) => o && typeof o.authInvoker === "string" ? { ...o, authInvoker: { namespace: ${JSON.stringify(authNamespace)}, machineUserName: o.authInvoker } } : o;\n`;
2739
2731
  }
2740
2732
  /**
2741
2733
  * Check if an AST binding pattern (parameter, catch clause, etc.) contains an Identifier with the given name.
@@ -2865,48 +2857,23 @@ function detectExtendedTriggerCalls(program, sourceText, workflowNames, jobNames
2865
2857
  const calls = [];
2866
2858
  function walk(node) {
2867
2859
  if (!node || typeof node !== "object") return;
2868
- if (node.type === "CallExpression") {
2869
- const callExpr = node;
2870
- const callee = callExpr.callee;
2871
- if (callee.type === "MemberExpression") {
2872
- const memberExpr = callee;
2873
- const identifierName = !memberExpr.computed && memberExpr.object.type === "Identifier" ? memberExpr.object.name : null;
2874
- const propertyName = !memberExpr.computed ? memberExpr.property.name : null;
2875
- if (identifierName && propertyName === "trigger") {
2876
- const isWorkflow = workflowNames.has(identifierName);
2877
- const isJob = jobNames.has(identifierName);
2878
- if (isWorkflow || isJob) {
2879
- const argCount = callExpr.arguments.length;
2880
- let argsText = "";
2881
- if (argCount > 0) {
2882
- const firstArg = callExpr.arguments[0];
2883
- if (firstArg && "start" in firstArg && "end" in firstArg) argsText = sourceText.slice(firstArg.start, firstArg.end);
2884
- }
2885
- if (isWorkflow && argCount >= 2) {
2886
- const secondArg = callExpr.arguments[1];
2887
- const authInvoker = extractAuthInvokerInfo(secondArg, sourceText);
2888
- if (authInvoker) calls.push({
2889
- kind: "workflow",
2890
- identifierName,
2891
- callRange: {
2892
- start: callExpr.start,
2893
- end: callExpr.end
2894
- },
2895
- argsText,
2896
- authInvoker
2897
- });
2898
- } else if (isJob) calls.push({
2899
- kind: "job",
2900
- identifierName,
2901
- callRange: {
2902
- start: callExpr.start,
2903
- end: callExpr.end
2904
- },
2905
- argsText
2906
- });
2907
- }
2908
- }
2909
- }
2860
+ const triggerCall = getTriggerCallInfo(node, sourceText);
2861
+ if (triggerCall) {
2862
+ const isWorkflow = workflowNames.has(triggerCall.identifierName);
2863
+ const isJob = jobNames.has(triggerCall.identifierName);
2864
+ if (isWorkflow) calls.push({
2865
+ kind: "workflow",
2866
+ identifierName: triggerCall.identifierName,
2867
+ callRange: triggerCall.callRange,
2868
+ argsText: triggerCall.argsText,
2869
+ optionsText: triggerCall.optionsText
2870
+ });
2871
+ else if (isJob) calls.push({
2872
+ kind: "job",
2873
+ identifierName: triggerCall.identifierName,
2874
+ callRange: triggerCall.callRange,
2875
+ argsText: triggerCall.argsText
2876
+ });
2910
2877
  }
2911
2878
  for (const key of Object.keys(node)) {
2912
2879
  const child = node[key];
@@ -2945,20 +2912,32 @@ function transformFunctionTriggers(source, workflowNameMap, jobNameMap, workflow
2945
2912
  }
2946
2913
  }
2947
2914
  }
2948
- const triggerCalls = detectExtendedTriggerCalls(program, source, new Set(localWorkflowNameMap.keys()), new Set(jobNameMap.keys()));
2915
+ const allTriggerCalls = detectExtendedTriggerCalls(program, source, new Set(localWorkflowNameMap.keys()), new Set(jobNameMap.keys()));
2916
+ const nestedTriggerCalls = [];
2917
+ const triggerCalls = allTriggerCalls.filter((call) => {
2918
+ const parent = allTriggerCalls.find((other) => other !== call && other.callRange.start <= call.callRange.start && call.callRange.end <= other.callRange.end);
2919
+ if (parent) {
2920
+ nestedTriggerCalls.push({
2921
+ call,
2922
+ parent
2923
+ });
2924
+ return false;
2925
+ }
2926
+ return true;
2927
+ });
2928
+ for (const { call, parent } of nestedTriggerCalls) logger.warn(`Nested trigger call "${call.identifierName}.trigger(...)" inside "${parent.identifierName}.trigger(...)" cannot be converted. Move it to a separate statement and pass the result instead.`);
2949
2929
  const replacements = [];
2950
2930
  let needsNormalizerHelper = false;
2951
2931
  const transformedCallsPerIdentifier = /* @__PURE__ */ new Map();
2952
- for (const call of triggerCalls) if (call.kind === "workflow" && call.authInvoker) {
2932
+ for (const call of triggerCalls) if (call.kind === "workflow") {
2953
2933
  const workflowName = localWorkflowNameMap.get(call.identifierName);
2954
2934
  if (workflowName) {
2955
- const rawExpr = call.authInvoker.isShorthand ? "authInvoker" : call.authInvoker.valueText;
2956
- let authInvokerExpr;
2957
- if (authNamespace) {
2958
- authInvokerExpr = `${NORMALIZER_IDENTIFIER}(${rawExpr})`;
2935
+ let optionsPart = "";
2936
+ if (call.optionsText !== void 0) if (authNamespace) {
2937
+ optionsPart = `, ${NORMALIZER_IDENTIFIER}(${call.optionsText})`;
2959
2938
  needsNormalizerHelper = true;
2960
- } else authInvokerExpr = rawExpr;
2961
- const transformedCall = `tailor.workflow.triggerWorkflow("${workflowName}", ${call.argsText || "undefined"}, { authInvoker: ${authInvokerExpr} })`;
2939
+ } else optionsPart = `, ${call.optionsText}`;
2940
+ const transformedCall = `tailor.workflow.triggerWorkflow("${workflowName}", ${call.argsText || "undefined"}${optionsPart})`;
2962
2941
  replacements.push({
2963
2942
  start: call.callRange.start,
2964
2943
  end: call.callRange.end,
@@ -2966,7 +2945,7 @@ function transformFunctionTriggers(source, workflowNameMap, jobNameMap, workflow
2966
2945
  });
2967
2946
  transformedCallsPerIdentifier.set(call.identifierName, (transformedCallsPerIdentifier.get(call.identifierName) ?? 0) + 1);
2968
2947
  }
2969
- } else if (call.kind === "job") {
2948
+ } else {
2970
2949
  const jobName = jobNameMap.get(call.identifierName);
2971
2950
  if (jobName) {
2972
2951
  const transformedCall = `(async () => tailor.workflow.triggerJobFunction("${jobName}", ${call.argsText || "undefined"}))()`;
@@ -3161,6 +3140,28 @@ async function bundleAuthHooks(options) {
3161
3140
  return bundledCode;
3162
3141
  }
3163
3142
 
3143
+ //#endregion
3144
+ //#region src/utils/script-expr.ts
3145
+ const MAX_GENERATED_CODE_LENGTH = 2e3;
3146
+ function formatGeneratedCode(expr) {
3147
+ if (expr.length <= MAX_GENERATED_CODE_LENGTH) return `Generated code:\n${expr}`;
3148
+ return `Generated code (truncated to ${MAX_GENERATED_CODE_LENGTH} of ${expr.length} characters):\n${expr.slice(0, MAX_GENERATED_CODE_LENGTH)}\n...`;
3149
+ }
3150
+ /**
3151
+ * Assert that a generated script expression is syntactically valid JavaScript.
3152
+ * Invalid generated code would otherwise surface much later as a confusing
3153
+ * bundler or platform-side syntax error, far from the definition that caused it.
3154
+ * @param expr - Generated JavaScript expression
3155
+ * @param context - What generated the expression, used to label the error
3156
+ * @returns The expression unchanged when it parses successfully
3157
+ */
3158
+ function assertParsableExpression(expr, context) {
3159
+ const { errors } = parseSync("generated-expr.js", `(${expr}\n);`);
3160
+ if (errors.length === 0) return expr;
3161
+ const details = errors.map((error) => ` - ${error.message}`).join("\n");
3162
+ throw new Error(`Generated ${context} script is not valid JavaScript.\nParse errors:\n${details}\n` + formatGeneratedCode(expr));
3163
+ }
3164
+
3164
3165
  //#endregion
3165
3166
  //#region src/parser/service/tailordb/hooks-validate-precompiled-expr.ts
3166
3167
  const PRECOMPILED_EXPR_KEY = "__precompiledScriptExpr";
@@ -3214,30 +3215,38 @@ const stringifyFunction = (fn) => {
3214
3215
  if (firstObjectProperty(`({m: ${src}})`)) return src;
3215
3216
  const wrapped = `({${src}})`;
3216
3217
  const property = firstObjectProperty(wrapped);
3217
- if (property?.type === "Property" && property.method && !property.computed && property.value.type === "FunctionExpression") {
3218
+ if (property?.type === "Property" && property.method && property.computed) throw new Error("Computed-key method shorthand cannot be converted to a TailorDB script expression. Use an arrow function or function expression instead.");
3219
+ if (property?.type === "Property" && property.method && property.value.type === "FunctionExpression") {
3218
3220
  const { async, generator } = property.value;
3219
3221
  const body = wrapped.slice(property.value.start, property.value.end);
3220
3222
  return `${async ? "async " : ""}function${generator ? "*" : ""} ${body}`;
3221
3223
  }
3222
3224
  return src;
3223
3225
  };
3226
+ function formatScriptContext(kind, context) {
3227
+ if (!context) return kind === "validate" ? kind : "hooks";
3228
+ return `${kind} for ${context.typeName}.${context.fieldPath.join(".")}`;
3229
+ }
3224
3230
  /**
3225
3231
  * Convert a hook or validator function to a script expression.
3226
3232
  * @param fn - Hook or validator function
3233
+ * @param kind - Label naming the source of the expression in conversion errors
3234
+ * @param context - Optional field context for conversion errors
3227
3235
  * @returns JavaScript expression calling the function
3228
3236
  */
3229
- const convertToScriptExpr = (fn) => {
3237
+ const convertToScriptExpr = (fn, kind, context) => {
3230
3238
  const precompiledExpr = getPrecompiledScriptExpr(fn);
3231
3239
  if (precompiledExpr) return precompiledExpr;
3232
- return `(${stringifyFunction(fn)})({ value: _value, data: _data, user: ${tailorUserMap} })`;
3240
+ return assertParsableExpression(`(${stringifyFunction(fn)})({ value: _value, data: _data, user: ${tailorUserMap} })`, formatScriptContext(kind, context));
3233
3241
  };
3234
3242
  /**
3235
3243
  * Parse TailorDBField into OperatorFieldConfig.
3236
3244
  * This transforms user-defined functions into script expressions.
3237
3245
  * @param field - TailorDB field definition
3246
+ * @param context - Optional field context for conversion errors
3238
3247
  * @returns Parsed operator field configuration
3239
3248
  */
3240
- function parseFieldConfig(field) {
3249
+ function parseFieldConfig(field, context) {
3241
3250
  const metadata = field.metadata;
3242
3251
  const fieldType = field.type;
3243
3252
  const rawRelation = field.rawRelation;
@@ -3247,7 +3256,10 @@ function parseFieldConfig(field) {
3247
3256
  ...metadata,
3248
3257
  rawRelation,
3249
3258
  ...fieldType === "nested" && nestedFields && Object.keys(nestedFields).length > 0 ? { fields: Object.entries(nestedFields).reduce((acc, [key, nestedField]) => {
3250
- acc[key] = parseFieldConfig(nestedField);
3259
+ acc[key] = parseFieldConfig(nestedField, context && {
3260
+ ...context,
3261
+ fieldPath: [...context.fieldPath, key]
3262
+ });
3251
3263
  return acc;
3252
3264
  }, {}) } : {},
3253
3265
  validate: metadata.validate?.map((v) => {
@@ -3259,13 +3271,13 @@ function parseFieldConfig(field) {
3259
3271
  message: v[1]
3260
3272
  };
3261
3273
  return {
3262
- script: { expr: convertToScriptExpr(fn) },
3274
+ script: { expr: convertToScriptExpr(fn, "validate", context) },
3263
3275
  errorMessage: message
3264
3276
  };
3265
3277
  }),
3266
3278
  hooks: metadata.hooks ? {
3267
- create: metadata.hooks.create ? { expr: convertToScriptExpr(metadata.hooks.create) } : void 0,
3268
- update: metadata.hooks.update ? { expr: convertToScriptExpr(metadata.hooks.update) } : void 0
3279
+ create: metadata.hooks.create ? { expr: convertToScriptExpr(metadata.hooks.create, "hooks.create", context) } : void 0,
3280
+ update: metadata.hooks.update ? { expr: convertToScriptExpr(metadata.hooks.update, "hooks.update", context) } : void 0
3269
3281
  } : void 0,
3270
3282
  serial: metadata.serial ? {
3271
3283
  start: metadata.serial.start,
@@ -3508,7 +3520,7 @@ function applyRelationMetadataToFieldConfig(fieldConfig, metadata) {
3508
3520
  function parseTypes(rawTypes, namespace, typeSourceInfo) {
3509
3521
  const types = createRecord();
3510
3522
  const allTypeNames = new Set(Object.keys(rawTypes));
3511
- for (const [typeName, type] of Object.entries(rawTypes)) types[typeName] = parseTailorDBType(type, allTypeNames, rawTypes);
3523
+ for (const [typeName, type] of Object.entries(rawTypes)) types[typeName] = parseTailorDBType(type, allTypeNames, rawTypes, typeSourceInfo);
3512
3524
  buildBackwardRelationships(types, namespace, typeSourceInfo);
3513
3525
  validatePluralFormUniqueness(types, namespace, typeSourceInfo);
3514
3526
  return types;
@@ -3518,21 +3530,26 @@ function parseTypes(rawTypes, namespace, typeSourceInfo) {
3518
3530
  * @param type - TailorDB type to parse
3519
3531
  * @param allTypeNames - Set of all TailorDB type names
3520
3532
  * @param rawTypes - All raw TailorDB types keyed by name
3533
+ * @param typeSourceInfo - Optional type source information
3521
3534
  * @returns Parsed TailorDB type
3522
3535
  */
3523
- function parseTailorDBType(type, allTypeNames, rawTypes) {
3536
+ function parseTailorDBType(type, allTypeNames, rawTypes, typeSourceInfo) {
3524
3537
  const metadata = type.metadata;
3525
3538
  const pluralForm = metadata.settings?.pluralForm || inflection.pluralize(type.name);
3539
+ const typeLocation = formatTypeSourceLocation(getTypeSourceInfo(typeSourceInfo, type.name));
3526
3540
  const fields = createRecord();
3527
3541
  const forwardRelationships = createRecord();
3528
3542
  for (const [fieldName, fieldDef] of Object.entries(type.fields)) {
3529
- let fieldConfig = parseFieldConfig(fieldDef);
3530
- const rawRelation = fieldConfig.rawRelation;
3531
3543
  const context = {
3532
3544
  typeName: type.name,
3533
3545
  fieldName,
3534
3546
  allTypeNames
3535
3547
  };
3548
+ let fieldConfig = parseFieldConfig(fieldDef, {
3549
+ typeName: type.name,
3550
+ fieldPath: [fieldName]
3551
+ });
3552
+ const rawRelation = fieldConfig.rawRelation;
3536
3553
  if (rawRelation) {
3537
3554
  validateRelationConfig(rawRelation, context);
3538
3555
  if ([
@@ -3552,9 +3569,18 @@ function parseTailorDBType(type, allTypeNames, rawTypes) {
3552
3569
  const relationInfo = rawRelation ? buildRelationInfo(rawRelation, context) : void 0;
3553
3570
  if (relationInfo) {
3554
3571
  parsedField.relation = { ...relationInfo };
3572
+ const forwardName = relationInfo.forwardName;
3573
+ if (forwardName.length === 0) throw new Error(`Forward relation name for field "${fieldName}" on type "${type.name}"${typeLocation} cannot be empty. Use the "as" option in .relation({ toward: { as: ... } }) to specify a non-empty name.`);
3574
+ const existingForward = forwardRelationships[forwardName];
3575
+ if (existingForward) throw new Error(`Forward relation name "${forwardName}" on type "${type.name}"${typeLocation} is duplicated between fields "${existingForward.targetField}" and "${fieldName}". Use the "as" option in .relation({ toward: { as: ... } }) to specify a unique name.`);
3576
+ if (Object.hasOwn(type.fields, forwardName)) {
3577
+ const message = forwardName === fieldName ? `Forward relation name "${forwardName}" on type "${type.name}"${typeLocation} is the same as its own relation field "${fieldName}". Use the "as" option in .relation({ toward: { as: ... } }) to specify a different name.` : `Forward relation name "${forwardName}" from field "${fieldName}" on type "${type.name}"${typeLocation} conflicts with existing field "${forwardName}". Use the "as" option in .relation({ toward: { as: ... } }) to specify a different name.`;
3578
+ throw new Error(message);
3579
+ }
3580
+ if (Object.hasOwn(metadata.files, forwardName)) throw new Error(`Forward relation name "${forwardName}" from field "${fieldName}" on type "${type.name}"${typeLocation} conflicts with files field "${forwardName}". Use the "as" option in .relation({ toward: { as: ... } }) to specify a different name.`);
3555
3581
  const targetType = rawTypes[relationInfo.targetType];
3556
- forwardRelationships[relationInfo.forwardName] = {
3557
- name: relationInfo.forwardName,
3582
+ forwardRelationships[forwardName] = {
3583
+ name: forwardName,
3558
3584
  targetType: relationInfo.targetType,
3559
3585
  targetField: fieldName,
3560
3586
  sourceField: relationInfo.key,
@@ -3615,13 +3641,11 @@ function buildBackwardRelationships(types, namespace, typeSourceInfo) {
3615
3641
  for (const [targetTypeName, backwardNames] of Object.entries(backwardNameSources)) {
3616
3642
  const targetType = types[targetTypeName];
3617
3643
  if (targetType === void 0) throw new Error(`type not found: ${targetTypeName}`);
3618
- const targetTypeSourceInfo = getTypeSourceInfo(typeSourceInfo, targetTypeName);
3619
- const targetLocation = targetTypeSourceInfo ? isPluginGeneratedType(targetTypeSourceInfo) ? ` (plugin: ${targetTypeSourceInfo.pluginId})` : ` (${targetTypeSourceInfo.filePath})` : "";
3644
+ const targetLocation = formatTypeSourceLocation(getTypeSourceInfo(typeSourceInfo, targetTypeName));
3620
3645
  for (const [backwardName, sources] of Object.entries(backwardNames)) {
3621
3646
  if (sources.length > 1) {
3622
3647
  const sourceList = sources.map((s) => {
3623
- const sourceInfo = getTypeSourceInfo(typeSourceInfo, s.sourceType);
3624
- const location = sourceInfo ? isPluginGeneratedType(sourceInfo) ? ` (plugin: ${sourceInfo.pluginId})` : ` (${sourceInfo.filePath})` : "";
3648
+ const location = formatTypeSourceLocation(getTypeSourceInfo(typeSourceInfo, s.sourceType));
3625
3649
  return `${s.sourceType}.${s.fieldName}${location}`;
3626
3650
  }).join(", ");
3627
3651
  errors.push(`Backward relation name "${backwardName}" on type "${targetTypeName}" is duplicated from: ${sourceList}. Use the "backward" option in .relation() to specify unique names.`);
@@ -3629,17 +3653,21 @@ function buildBackwardRelationships(types, namespace, typeSourceInfo) {
3629
3653
  if (Object.hasOwn(targetType.fields, backwardName)) {
3630
3654
  const source = sources[0];
3631
3655
  if (source === void 0) throw new Error(`no source found for backward name: ${backwardName}`);
3632
- const sourceInfo = getTypeSourceInfo(typeSourceInfo, source.sourceType);
3633
- const sourceLocation = sourceInfo ? isPluginGeneratedType(sourceInfo) ? ` (plugin: ${sourceInfo.pluginId})` : ` (${sourceInfo.filePath})` : "";
3656
+ const sourceLocation = formatTypeSourceLocation(getTypeSourceInfo(typeSourceInfo, source.sourceType));
3634
3657
  errors.push(`Backward relation name "${backwardName}" from ${source.sourceType}.${source.fieldName}${sourceLocation} conflicts with existing field "${backwardName}" on type "${targetTypeName}"${targetLocation}. Use the "backward" option in .relation() to specify a different name.`);
3635
3658
  }
3636
3659
  if (targetType.files && Object.hasOwn(targetType.files, backwardName)) {
3637
3660
  const source = sources[0];
3638
3661
  if (source === void 0) throw new Error(`no source found for backward name: ${backwardName}`);
3639
- const sourceInfo = getTypeSourceInfo(typeSourceInfo, source.sourceType);
3640
- const sourceLocation = sourceInfo ? isPluginGeneratedType(sourceInfo) ? ` (plugin: ${sourceInfo.pluginId})` : ` (${sourceInfo.filePath})` : "";
3662
+ const sourceLocation = formatTypeSourceLocation(getTypeSourceInfo(typeSourceInfo, source.sourceType));
3641
3663
  errors.push(`Backward relation name "${backwardName}" from ${source.sourceType}.${source.fieldName}${sourceLocation} conflicts with files field "${backwardName}" on type "${targetTypeName}"${targetLocation}. Use the "backward" option in .relation() to specify a different name.`);
3642
3664
  }
3665
+ if (Object.hasOwn(targetType.forwardRelationships, backwardName)) {
3666
+ const source = sources[0];
3667
+ if (source === void 0) throw new Error(`no source found for backward name: ${backwardName}`);
3668
+ const sourceLocation = formatTypeSourceLocation(getTypeSourceInfo(typeSourceInfo, source.sourceType));
3669
+ errors.push(`Relation name "${backwardName}" on type "${targetTypeName}"${targetLocation} is used by both a forward relationship and a backward relationship from ${source.sourceType}.${source.fieldName}${sourceLocation}. Use the "as" option in .relation({ toward: { as: ... } }) or the "backward" option in .relation() to specify unique names.`);
3670
+ }
3643
3671
  }
3644
3672
  }
3645
3673
  if (errors.length > 0) throw new Error(`Backward relation name conflicts detected in TailorDB service "${namespace}".\n${errors.map((e) => ` - ${e}`).join("\n")}`);
@@ -3658,8 +3686,7 @@ function validatePluralFormUniqueness(types, namespace, typeSourceInfo) {
3658
3686
  for (const [, parsedType] of Object.entries(types)) {
3659
3687
  const singularQuery = inflection.camelize(parsedType.name, true);
3660
3688
  if (singularQuery === inflection.camelize(parsedType.pluralForm, true)) {
3661
- const sourceInfo = getTypeSourceInfo(typeSourceInfo, parsedType.name);
3662
- const location = sourceInfo ? isPluginGeneratedType(sourceInfo) ? ` (plugin: ${sourceInfo.pluginId})` : ` (${sourceInfo.filePath})` : "";
3689
+ const location = formatTypeSourceLocation(getTypeSourceInfo(typeSourceInfo, parsedType.name));
3663
3690
  errors.push(`Type "${parsedType.name}"${location} has identical singular and plural query names "${singularQuery}". Use db.type(["${parsedType.name}", "UniquePluralForm"], {...}) to set a unique pluralForm.`);
3664
3691
  }
3665
3692
  }
@@ -3685,8 +3712,7 @@ function validatePluralFormUniqueness(types, namespace, typeSourceInfo) {
3685
3712
  const duplicates = [...queryNameToSource].filter(([, sources]) => sources.length > 1);
3686
3713
  for (const [queryName, sources] of duplicates) {
3687
3714
  const sourceList = sources.map((s) => {
3688
- const sourceInfo = getTypeSourceInfo(typeSourceInfo, s.typeName);
3689
- const location = sourceInfo ? isPluginGeneratedType(sourceInfo) ? ` (plugin: ${sourceInfo.pluginId})` : ` (${sourceInfo.filePath})` : "";
3715
+ const location = formatTypeSourceLocation(getTypeSourceInfo(typeSourceInfo, s.typeName));
3690
3716
  return `"${s.typeName}"${location} (${s.kind})`;
3691
3717
  }).join(", ");
3692
3718
  errors.push(`GraphQL query field "${queryName}" conflicts between: ${sourceList}`);
@@ -3696,6 +3722,10 @@ function validatePluralFormUniqueness(types, namespace, typeSourceInfo) {
3696
3722
  function getTypeSourceInfo(typeSourceInfo, typeName) {
3697
3723
  return typeSourceInfo && Object.hasOwn(typeSourceInfo, typeName) ? typeSourceInfo[typeName] : void 0;
3698
3724
  }
3725
+ function formatTypeSourceLocation(sourceInfo) {
3726
+ if (!sourceInfo) return "";
3727
+ return isPluginGeneratedType(sourceInfo) ? ` (plugin: ${sourceInfo.pluginId})` : ` (${sourceInfo.filePath})`;
3728
+ }
3699
3729
  function createRecord() {
3700
3730
  return Object.create(null);
3701
3731
  }
@@ -4211,12 +4241,13 @@ function buildMinimalEntryFromResolved(imports, declarations, fnSource, sourceFi
4211
4241
  }
4212
4242
  async function bundleScriptTarget(args) {
4213
4243
  const { fn, kind, sourceFilePath, sourceBindings, tempDir, targetIndex, tsconfig } = args;
4244
+ const context = `${kind} in ${sourceFilePath}`;
4214
4245
  const fnSource = stringifyFunction(fn);
4215
- const inlineExpr = `(${fnSource})({ value: _value, data: _data, user: ${tailorUserMap} })`;
4246
+ const inlineExpr = assertParsableExpression(`(${fnSource})({ value: _value, data: _data, user: ${tailorUserMap} })`, context);
4216
4247
  const freeVars = findUndefinedReferences(`const __fn = ${fnSource};`);
4217
4248
  if (freeVars.size === 0) return inlineExpr;
4218
4249
  const { imports, declarations, unresolved } = resolveNeededBindings(freeVars, sourceBindings);
4219
- if (unresolved.length > 0) throw new Error(`${kind} in ${sourceFilePath} captures unresolvable variables (${unresolved.join(", ")}). Hooks and validators must not reference variables that cannot be resolved from the source file.
4250
+ if (unresolved.length > 0) throw new Error(`${context} captures unresolvable variables (${unresolved.join(", ")}). Hooks and validators must not reference variables that cannot be resolved from the source file.
4220
4251
  ${kind}: ${fnSource}`);
4221
4252
  const entryContent = buildMinimalEntryFromResolved(imports, declarations, fnSource, sourceFilePath);
4222
4253
  const entryPath = join(tempDir, `tailordb-script-${targetIndex}.entry.ts`);
@@ -4239,7 +4270,7 @@ async function bundleScriptTarget(args) {
4239
4270
  },
4240
4271
  logLevel: "silent"
4241
4272
  })).output[0].code;
4242
- return buildPrecompiledExpr(bundledCode);
4273
+ return assertParsableExpression(buildPrecompiledExpr(bundledCode), context);
4243
4274
  }
4244
4275
  /**
4245
4276
  * Precompile TailorDB hooks/validators into self-contained script expressions using rolldown.
@@ -5444,11 +5475,17 @@ function transformWorkflowSource(source, targetJobName, targetJobExportName, oth
5444
5475
  end: endPos,
5445
5476
  text: ""
5446
5477
  });
5447
- } else if (!job.statementRange) replacements.push({
5448
- start: job.bodyValueRange.start,
5449
- end: job.bodyValueRange.end,
5450
- text: "() => {}"
5451
- });
5478
+ } else if (!job.statementRange) {
5479
+ removedRanges.push({
5480
+ start: job.bodyValueRange.start,
5481
+ end: job.bodyValueRange.end
5482
+ });
5483
+ replacements.push({
5484
+ start: job.bodyValueRange.start,
5485
+ end: job.bodyValueRange.end,
5486
+ text: "() => {}"
5487
+ });
5488
+ }
5452
5489
  }
5453
5490
  if (otherJobExportNames) for (const exportName of otherJobExportNames) {
5454
5491
  if (exportName === targetJobExportName) continue;
@@ -6455,4 +6492,4 @@ async function loadApplication(params) {
6455
6492
 
6456
6493
  //#endregion
6457
6494
  export { initOperatorClient as $, loadAccessToken as A, saveUserTokens as B, hashContent as C, fetchLatestToken as D, deleteUserTokens as E, loadStoredUserTokens as F, fetchMachineUserToken as G, closeConnectionPool as H, loadWorkspaceId as I, fetchUserInfo as J, fetchPaged as K, platformConfigFromProfile as L, loadConsoleBaseUrl as M, loadMachineUserName as N, hasAnyUserTokenEntry as O, loadPlatformClientConfig as P, initOAuth2Client as Q, readPlatformConfig as R, getDistDir as S, loadConfig as T, defaultPlatformBaseUrl as U, writePlatformConfig as V, fetchAll as W, getOAuth2ClientId as X, getConsoleBaseUrl as Y, getPlatformBaseUrl as Z, createLogLevelTreeshakeOptions as _, WorkflowJobSchema as a, hasGenerationHooks as b, INVOKER_EXPR as c, assertUniqueLocalTailorDBTypeNames as d, isDefaultPlatform as et, assertUniqueTailorDBTypeNamesWithExternal as f, composeFunctionTreeshakeOptions as g, platformBundleDefinePlugin as h, resolveInlineSourcemap as i, loadConfigPath as j, hasUserTokenEntry as k, buildExecutorArgsExpr as l, stringifyFunction as m, generatePluginFilesIfNeeded as n, byName as nt, ResolverSchema as o, TailorDBTypeSchema as p, fetchPlatformMachineUserToken as q, loadApplication as r, HTTP_METHODS as s, defineApplication as t, resolveStaticWebsiteUrls as tt, buildResolverOperationHookExpr as u, resolveBundleLogLevel as v, hashFile as w, createBundleCache as x, getPluginGenerationDependencies as y, resolveUserTokenKey as z };
6458
- //# sourceMappingURL=application-Bb9NNp5m.mjs.map
6495
+ //# sourceMappingURL=application-DgTCDMZY.mjs.map