appflare 0.2.45 → 0.2.47

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.
@@ -1,9 +1,47 @@
1
- import { DiscoveredHandlerOperation } from "../../../../../utils/handler-discovery";
1
+ import { DiscoveredArgField, DiscoveredHandlerOperation } from "../../../../../utils/handler-discovery";
2
2
 
3
- function renderArgInputType(type: string): string {
4
- if (type === "boolean") return "checkbox";
5
- if (type === "number") return "number";
6
- return "text";
3
+ const fieldInputClass =
4
+ "input input-xs input-bordered font-mono flex-1 bg-base-200/30 focus:bg-base-100 focus:border-primary transition-all rounded-lg border-base-200";
5
+
6
+ function renderObjectFieldInputs(fields: DiscoveredArgField[]): string {
7
+ return fields
8
+ .map((f) => {
9
+ const badge =
10
+ f.type !== "unknown"
11
+ ? `<span class="badge badge-xs badge-ghost font-mono opacity-25 ml-1">${f.type}</span>`
12
+ : "";
13
+
14
+ if (f.type === "boolean") {
15
+ return `
16
+ <div class="flex items-center gap-2">
17
+ <input
18
+ type="checkbox"
19
+ data-obj-key="${f.name}"
20
+ data-obj-field-type="boolean"
21
+ class="checkbox checkbox-xs checkbox-primary shrink-0"
22
+ ${f.defaultValue === "true" ? "checked" : ""}
23
+ />
24
+ <span class="text-[10px] font-mono opacity-50">${f.name}${badge}</span>
25
+ </div>`;
26
+ }
27
+
28
+ const inputType =
29
+ f.type === "number" ? "number" : f.type === "date" ? "datetime-local" : "text";
30
+
31
+ return `
32
+ <div class="flex items-center gap-2">
33
+ <span class="text-[10px] font-mono opacity-50 w-20 shrink-0 truncate" title="${f.name}">${f.name}${badge}</span>
34
+ <input
35
+ type="${inputType}"
36
+ data-obj-key="${f.name}"
37
+ data-obj-field-type="${f.type}"
38
+ placeholder="${f.defaultValue ?? ""}"
39
+ value="${f.defaultValue ?? ""}"
40
+ class="${fieldInputClass}"
41
+ />
42
+ </div>`;
43
+ })
44
+ .join("\n");
7
45
  }
8
46
 
9
47
  function renderArgRows(h: DiscoveredHandlerOperation): string {
@@ -14,17 +52,26 @@ function renderArgRows(h: DiscoveredHandlerOperation): string {
14
52
  `;
15
53
  }
16
54
 
55
+ const inputClass =
56
+ "input input-sm input-bordered font-mono w-full bg-base-200/30 focus:bg-base-100 focus:border-primary transition-all rounded-xl border-base-200";
57
+
17
58
  return fields
18
59
  .map((field) => {
19
- const inputType = renderArgInputType(field.type);
20
- const isCheckbox = inputType === "checkbox";
21
60
  const label = `${field.name}${field.optional ? "" : " *"}`;
22
61
  const badge =
23
62
  field.type !== "unknown"
24
63
  ? `<span class="badge badge-xs badge-ghost font-mono opacity-40 ml-1">${field.type}</span>`
25
64
  : "";
65
+ const optionalAlt = field.optional
66
+ ? '<span class="label-text-alt text-[10px] opacity-30 italic">optional</span>'
67
+ : "";
68
+ const labelRow = `
69
+ <label class="label py-0.5">
70
+ <span class="label-text text-[11px] font-mono font-semibold">${label}${badge}</span>
71
+ ${optionalAlt}
72
+ </label>`;
26
73
 
27
- if (isCheckbox) {
74
+ if (field.type === "boolean") {
28
75
  return `
29
76
  <div class="flex items-center gap-3 py-1">
30
77
  <input
@@ -36,27 +83,80 @@ function renderArgRows(h: DiscoveredHandlerOperation): string {
36
83
  />
37
84
  <span class="text-sm font-mono opacity-70">${field.name}${badge}</span>
38
85
  ${field.optional ? '<span class="text-[10px] opacity-30 italic ml-auto">optional</span>' : ""}
39
- </div>
40
- `;
86
+ </div>`;
87
+ }
88
+
89
+ if (field.type === "object") {
90
+ if (field.fields && field.fields.length > 0) {
91
+ return `
92
+ <div class="form-control">
93
+ ${labelRow}
94
+ <div
95
+ data-arg-key="${field.name}"
96
+ data-arg-type="object"
97
+ class="border border-base-200 rounded-xl p-3 bg-base-200/20 flex flex-col gap-2"
98
+ >
99
+ ${renderObjectFieldInputs(field.fields)}
100
+ </div>
101
+ </div>`;
102
+ }
103
+ // Fallback when schema isn't statically known
104
+ return `
105
+ <div class="form-control">
106
+ ${labelRow}
107
+ <textarea
108
+ data-arg-key="${field.name}"
109
+ data-arg-type="object"
110
+ placeholder="{}"
111
+ rows="4"
112
+ class="textarea textarea-sm textarea-bordered font-mono w-full bg-base-200/30 focus:bg-base-100 focus:border-primary transition-all rounded-xl border-base-200 resize-y text-xs"
113
+ ${!field.optional ? "required" : ""}
114
+ >${field.defaultValue ?? ""}</textarea>
115
+ </div>`;
41
116
  }
42
117
 
118
+ if (field.type === "array") {
119
+ const itemType = field.itemType ?? "string";
120
+ const itemFieldsJson = JSON.stringify(
121
+ (field.itemFields ?? []).map((f) => ({ name: f.name, type: f.type })),
122
+ );
123
+ return `
124
+ <div class="form-control">
125
+ ${labelRow}
126
+ <div
127
+ data-arg-key="${field.name}"
128
+ data-arg-type="array"
129
+ data-item-type="${itemType}"
130
+ data-item-fields='${itemFieldsJson}'
131
+ class="flex flex-col gap-2"
132
+ ></div>
133
+ <button
134
+ type="button"
135
+ onclick="addArrayItem('${field.name}')"
136
+ class="btn btn-xs btn-ghost gap-1 mt-1 self-start opacity-50 hover:opacity-100"
137
+ >
138
+ <iconify-icon icon="solar:add-circle-linear" width="12" height="12"></iconify-icon>
139
+ Add item
140
+ </button>
141
+ </div>`;
142
+ }
143
+
144
+ const inputType =
145
+ field.type === "number" ? "number" : field.type === "date" ? "datetime-local" : "text";
146
+
43
147
  return `
44
148
  <div class="form-control">
45
- <label class="label py-0.5">
46
- <span class="label-text text-[11px] font-mono font-semibold">${label}${badge}</span>
47
- ${field.optional ? '<span class="label-text-alt text-[10px] opacity-30 italic">optional</span>' : ""}
48
- </label>
149
+ ${labelRow}
49
150
  <input
50
151
  type="${inputType}"
51
152
  placeholder="${field.defaultValue ?? ""}"
52
153
  data-arg-key="${field.name}"
53
154
  data-arg-type="${field.type}"
54
155
  value="${field.defaultValue ?? ""}"
55
- class="input input-sm input-bordered font-mono w-full bg-base-200/30 focus:bg-base-100 focus:border-primary transition-all rounded-xl border-base-200"
156
+ class="${inputClass}"
56
157
  ${!field.optional ? "required" : ""}
57
158
  />
58
- </div>
59
- `;
159
+ </div>`;
60
160
  })
61
161
  .join("\n");
62
162
  }
@@ -74,6 +74,29 @@ export function renderScripts(h: DiscoveredHandlerOperation): string {
74
74
  container.appendChild(row);
75
75
  };
76
76
 
77
+ // ── Field-level value helpers ──────────────────────────────────────────────
78
+ function getFieldValue(el) {
79
+ var type = el.getAttribute('data-obj-field-type') || 'string';
80
+ if (type === 'boolean') return el.checked;
81
+ if (type === 'number') {
82
+ var raw = el.value.trim();
83
+ return raw === '' ? undefined : Number(raw);
84
+ }
85
+ var v = el.value.trim();
86
+ return v === '' ? undefined : v;
87
+ }
88
+
89
+ function collectObjectRow(container) {
90
+ var obj = {};
91
+ container.querySelectorAll('[data-obj-key]').forEach(function(f) {
92
+ var k = f.getAttribute('data-obj-key');
93
+ if (!k) return;
94
+ var v = getFieldValue(f);
95
+ if (v !== undefined) obj[k] = v;
96
+ });
97
+ return Object.keys(obj).length ? obj : undefined;
98
+ }
99
+
77
100
  // ── Collect args from server-rendered inputs ───────────────────────────────
78
101
  function collectArgs() {
79
102
  var result = {};
@@ -87,6 +110,36 @@ export function renderScripts(h: DiscoveredHandlerOperation): string {
87
110
  } else if (type === 'number') {
88
111
  var raw = el.value.trim();
89
112
  value = raw === '' ? undefined : Number(raw);
113
+ } else if (type === 'object') {
114
+ if (el.tagName === 'TEXTAREA') {
115
+ // Fallback JSON textarea
116
+ var raw = el.value.trim();
117
+ if (!raw) return;
118
+ try { value = JSON.parse(raw); } catch(e) { value = raw; }
119
+ } else {
120
+ // Structured field inputs
121
+ value = collectObjectRow(el);
122
+ }
123
+ } else if (type === 'array') {
124
+ var itemType = el.getAttribute('data-item-type') || 'string';
125
+ if (itemType === 'object') {
126
+ var rows = el.querySelectorAll('.array-item-row');
127
+ var arr = Array.from(rows).map(function(row) {
128
+ return collectObjectRow(row);
129
+ }).filter(Boolean);
130
+ value = arr.length ? arr : undefined;
131
+ } else {
132
+ var items = el.querySelectorAll('.array-item-input');
133
+ var arr = Array.from(items).map(function(inp) {
134
+ var ft = inp.getAttribute('data-obj-field-type') || 'string';
135
+ if (ft === 'number') {
136
+ var raw = inp.value.trim();
137
+ return raw === '' ? undefined : Number(raw);
138
+ }
139
+ return inp.value.trim() || undefined;
140
+ }).filter(function(v) { return v !== undefined; });
141
+ value = arr.length ? arr : undefined;
142
+ }
90
143
  } else {
91
144
  value = el.value;
92
145
  }
@@ -97,6 +150,102 @@ export function renderScripts(h: DiscoveredHandlerOperation): string {
97
150
  return result;
98
151
  }
99
152
 
153
+ // ── Array / object item helpers ────────────────────────────────────────────
154
+ function makeFieldInput(field) {
155
+ if (field.type === 'boolean') {
156
+ var cb = document.createElement('input');
157
+ cb.type = 'checkbox';
158
+ cb.className = 'checkbox checkbox-xs checkbox-primary shrink-0';
159
+ cb.setAttribute('data-obj-key', field.name);
160
+ cb.setAttribute('data-obj-field-type', 'boolean');
161
+ return cb;
162
+ }
163
+ var inputType = field.type === 'number' ? 'number'
164
+ : field.type === 'date' ? 'datetime-local'
165
+ : 'text';
166
+ var inp = document.createElement('input');
167
+ inp.type = inputType;
168
+ inp.placeholder = field.name;
169
+ inp.className = 'input input-xs input-bordered font-mono flex-1 bg-base-200/30 focus:bg-base-100 focus:border-primary transition-all rounded-lg border-base-200';
170
+ inp.setAttribute('data-obj-key', field.name);
171
+ inp.setAttribute('data-obj-field-type', field.type);
172
+ return inp;
173
+ }
174
+
175
+ window.addArrayItem = function(key) {
176
+ var container = document.querySelector('[data-arg-key="' + key + '"][data-arg-type="array"]');
177
+ if (!container) return;
178
+ var itemType = container.getAttribute('data-item-type') || 'string';
179
+
180
+ if (itemType === 'object') {
181
+ var fieldsJson = container.getAttribute('data-item-fields') || '[]';
182
+ var itemFields;
183
+ try { itemFields = JSON.parse(fieldsJson); } catch(e) { itemFields = []; }
184
+
185
+ var rowCount = container.querySelectorAll('.array-item-row').length + 1;
186
+
187
+ var row = document.createElement('div');
188
+ row.className = 'array-item-row border border-base-200 rounded-xl p-3 bg-base-200/20 flex flex-col gap-2';
189
+
190
+ var header = document.createElement('div');
191
+ header.className = 'flex items-center justify-between';
192
+
193
+ var lbl = document.createElement('span');
194
+ lbl.className = 'text-[10px] font-mono opacity-30';
195
+ lbl.textContent = 'Item ' + rowCount;
196
+
197
+ var removeBtn = document.createElement('button');
198
+ removeBtn.type = 'button';
199
+ removeBtn.className = 'btn btn-xs btn-ghost text-error opacity-40 hover:opacity-100 px-1.5';
200
+ removeBtn.innerHTML = '<iconify-icon icon="solar:close-circle-linear" width="14" height="14"></iconify-icon>';
201
+ removeBtn.onclick = function() { row.remove(); };
202
+
203
+ header.appendChild(lbl);
204
+ header.appendChild(removeBtn);
205
+ row.appendChild(header);
206
+
207
+ itemFields.forEach(function(field) {
208
+ var fieldRow = document.createElement('div');
209
+ fieldRow.className = 'flex items-center gap-2';
210
+
211
+ var fieldLbl = document.createElement('span');
212
+ fieldLbl.className = 'text-[10px] font-mono opacity-50 w-20 shrink-0 truncate';
213
+ fieldLbl.title = field.name;
214
+ fieldLbl.textContent = field.name;
215
+
216
+ fieldRow.appendChild(fieldLbl);
217
+ fieldRow.appendChild(makeFieldInput(field));
218
+ row.appendChild(fieldRow);
219
+ });
220
+
221
+ container.appendChild(row);
222
+ } else {
223
+ var inputType = itemType === 'number' ? 'number'
224
+ : itemType === 'date' ? 'datetime-local'
225
+ : 'text';
226
+
227
+ var row = document.createElement('div');
228
+ row.className = 'flex items-center gap-2';
229
+
230
+ var inp = document.createElement('input');
231
+ inp.type = inputType;
232
+ inp.className = 'array-item-input input input-xs input-bordered font-mono flex-1 bg-base-200/30 focus:bg-base-100 focus:border-primary transition-all rounded-lg border-base-200';
233
+ inp.placeholder = 'value';
234
+ inp.setAttribute('data-obj-field-type', itemType);
235
+
236
+ var removeBtn = document.createElement('button');
237
+ removeBtn.type = 'button';
238
+ removeBtn.className = 'btn btn-xs btn-ghost text-error opacity-40 hover:opacity-100 px-1.5';
239
+ removeBtn.innerHTML = '<iconify-icon icon="solar:close-circle-linear" width="14" height="14"></iconify-icon>';
240
+ removeBtn.onclick = function() { row.remove(); };
241
+
242
+ row.appendChild(inp);
243
+ row.appendChild(removeBtn);
244
+ container.appendChild(row);
245
+ inp.focus();
246
+ }
247
+ };
248
+
100
249
  function collectHeaders() {
101
250
  var result = {};
102
251
  document.querySelectorAll('#headers-rows [data-hdr-key]').forEach(function(keyEl) {
@@ -18,6 +18,15 @@ export async function executeCronTriggers(
18
18
 
19
19
  try {
20
20
  await cronEntry.definition.handler(ctx);
21
+
22
+ if (typeof publishMutationEvents === "function") {
23
+ await publishMutationEvents(
24
+ { req: { raw: new Request("http://localhost") }, env },
25
+ options,
26
+ ctx.mutationEvents,
27
+ );
28
+ }
29
+ ctx.mutationEvents.length = 0;
21
30
  } catch (error) {
22
31
  console.error("Cron task failed", cronEntry.taskName, error);
23
32
  }
@@ -43,6 +43,15 @@ export async function executeScheduledBatch(
43
43
  const payloadValue = body.payload === null ? undefined : body.payload;
44
44
  const parsed = operation.schema.parse(payloadValue);
45
45
  await operation.definition.handler(ctx, parsed);
46
+
47
+ if (typeof publishMutationEvents === "function") {
48
+ await publishMutationEvents(
49
+ { req: { raw: new Request("http://localhost") }, env },
50
+ options,
51
+ ctx.mutationEvents,
52
+ );
53
+ }
54
+ ctx.mutationEvents.length = 0;
46
55
  } catch (error) {
47
56
  if (error instanceof ZodError) {
48
57
  console.error("Invalid scheduler payload", task, error.issues);
@@ -13,9 +13,15 @@ export type HandlerKind =
13
13
 
14
14
  export type DiscoveredArgField = {
15
15
  name: string;
16
- type: "string" | "number" | "boolean" | "unknown";
16
+ type: "string" | "number" | "boolean" | "date" | "object" | "array" | "unknown";
17
17
  optional: boolean;
18
18
  defaultValue?: string;
19
+ /** Populated when type === "object": schema of the object's properties */
20
+ fields?: DiscoveredArgField[];
21
+ /** Populated when type === "array": element type */
22
+ itemType?: "string" | "number" | "boolean" | "date" | "object" | "unknown";
23
+ /** Populated when type === "array" and itemType === "object": element object schema */
24
+ itemFields?: DiscoveredArgField[];
19
25
  };
20
26
 
21
27
  export type DiscoveredHandlerOperation = {
@@ -83,6 +89,9 @@ function readZodArgField(
83
89
  let optional = false;
84
90
  let defaultValue: string | undefined;
85
91
  let baseType: DiscoveredArgField["type"] = "unknown";
92
+ let fields: DiscoveredArgField[] | undefined;
93
+ let itemType: DiscoveredArgField["itemType"] | undefined;
94
+ let itemFields: DiscoveredArgField[] | undefined;
86
95
 
87
96
  // Walk the call chain: z.string().optional().default("x")
88
97
  // Each iteration processes one layer (e.g. .default, .optional, .string)
@@ -120,13 +129,39 @@ function readZodArgField(
120
129
  } else if (prop === "boolean") {
121
130
  baseType = "boolean";
122
131
  break;
132
+ } else if (prop === "date") {
133
+ baseType = "date";
134
+ break;
135
+ } else if (prop === "object") {
136
+ baseType = "object";
137
+ const objArg = node.arguments[0];
138
+ if (objArg && ts.isObjectLiteralExpression(objArg)) {
139
+ fields = [];
140
+ for (const property of objArg.properties) {
141
+ if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
142
+ fields.push(readZodArgField(property.initializer, property.name.text));
143
+ }
144
+ }
145
+ }
146
+ break;
147
+ } else if (prop === "array") {
148
+ baseType = "array";
149
+ const itemArg = node.arguments[0];
150
+ if (itemArg) {
151
+ const itemField = readZodArgField(itemArg, "");
152
+ itemType = itemField.type === "array" ? "unknown" : itemField.type;
153
+ if (itemField.type === "object") {
154
+ itemFields = itemField.fields;
155
+ }
156
+ }
157
+ break;
123
158
  } else {
124
159
  // unknown modifier (.min, .max, .trim, etc.) — keep walking inward
125
160
  node = expr.expression;
126
161
  }
127
162
  }
128
163
 
129
- return { name, type: baseType, optional, defaultValue };
164
+ return { name, type: baseType, optional, defaultValue, fields, itemType, itemFields };
130
165
  }
131
166
 
132
167
  function readArgsFields(