@xano/developer-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/api_docs/addon.md +193 -0
- package/api_docs/agent.md +154 -0
- package/api_docs/api_group.md +236 -0
- package/api_docs/authentication.md +68 -0
- package/api_docs/file.md +190 -0
- package/api_docs/function.md +217 -0
- package/api_docs/history.md +263 -0
- package/api_docs/index.md +104 -0
- package/api_docs/mcp_server.md +139 -0
- package/api_docs/middleware.md +205 -0
- package/api_docs/realtime.md +153 -0
- package/api_docs/table.md +151 -0
- package/api_docs/task.md +191 -0
- package/api_docs/tool.md +216 -0
- package/api_docs/triggers.md +344 -0
- package/api_docs/workspace.md +246 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +495 -0
- package/package.json +49 -0
- package/xanoscript_docs/README.md +1 -0
- package/xanoscript_docs/api_query_examples.md +1255 -0
- package/xanoscript_docs/api_query_guideline.md +129 -0
- package/xanoscript_docs/build_from_lovable.md +715 -0
- package/xanoscript_docs/db_query_guideline.md +427 -0
- package/xanoscript_docs/ephemeral_environment_guideline.md +529 -0
- package/xanoscript_docs/expression_guideline.md +1086 -0
- package/xanoscript_docs/frontend_guideline.md +67 -0
- package/xanoscript_docs/function_examples.md +1406 -0
- package/xanoscript_docs/function_guideline.md +130 -0
- package/xanoscript_docs/functions.md +2155 -0
- package/xanoscript_docs/input_guideline.md +227 -0
- package/xanoscript_docs/mcp_server_examples.md +36 -0
- package/xanoscript_docs/mcp_server_guideline.md +69 -0
- package/xanoscript_docs/query_filter.md +489 -0
- package/xanoscript_docs/table_examples.md +586 -0
- package/xanoscript_docs/table_guideline.md +137 -0
- package/xanoscript_docs/task_examples.md +511 -0
- package/xanoscript_docs/task_guideline.md +103 -0
- package/xanoscript_docs/tips_and_tricks.md +144 -0
- package/xanoscript_docs/tool_examples.md +69 -0
- package/xanoscript_docs/tool_guideline.md +139 -0
- package/xanoscript_docs/unit_testing_guideline.md +328 -0
- package/xanoscript_docs/version.json +3 -0
- package/xanoscript_docs/workspace.md +17 -0
|
@@ -0,0 +1,1255 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "apis/**/*.xs"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Xanoscript API Query Examples
|
|
6
|
+
|
|
7
|
+
Below are some examples of API queries that can be made using Xanoscript.
|
|
8
|
+
|
|
9
|
+
## random_text_selector
|
|
10
|
+
|
|
11
|
+
## blog_posts_list
|
|
12
|
+
|
|
13
|
+
```xs
|
|
14
|
+
query "blog_posts" verb=GET {
|
|
15
|
+
description = "Returns a paginated list of all blog posts with paging and sorting capabilities. Includes author information, comment counts, and like counts for each post."
|
|
16
|
+
input {
|
|
17
|
+
int page?=1 {
|
|
18
|
+
description = "Page number for pagination"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
int per_page?=10 {
|
|
22
|
+
description = "Number of items per page (max 100)"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
json sort? {
|
|
26
|
+
description = "External sorting configuration array with orderBy and sortBy fields"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
stack {
|
|
31
|
+
db.query blog_post {
|
|
32
|
+
join = {
|
|
33
|
+
user: {
|
|
34
|
+
table: "user"
|
|
35
|
+
where: $db.blog_post.author_id == $db.user.id
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
sort = {blog_post.publication_date: "desc"}
|
|
40
|
+
override_sort = $input.sort
|
|
41
|
+
eval = {authorName: $db.user.name, status: $db.user.status}
|
|
42
|
+
return = {
|
|
43
|
+
type : "list"
|
|
44
|
+
paging: {
|
|
45
|
+
page : $input.page
|
|
46
|
+
per_page: $input.per_page
|
|
47
|
+
totals : true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
addon = [
|
|
52
|
+
{
|
|
53
|
+
name : "blog_post_likes"
|
|
54
|
+
input: {blog_post_id: $output.id}
|
|
55
|
+
as : "items.blog_post_likes"
|
|
56
|
+
}
|
|
57
|
+
{
|
|
58
|
+
name : "blog_post_comments"
|
|
59
|
+
input: {blog_post_id: $output.id}
|
|
60
|
+
as : "items.blog_post_comments"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
} as $posts
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
response = $posts
|
|
67
|
+
|
|
68
|
+
history = false
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## review_by_product
|
|
73
|
+
|
|
74
|
+
```xs
|
|
75
|
+
query "reviews/by_product/{product_id}" verb=GET {
|
|
76
|
+
description = "Retrieve all reviews for a given product with optional minimum rating, sorted by review_date descending, and paginated."
|
|
77
|
+
input {
|
|
78
|
+
int product_id {
|
|
79
|
+
description = "ID of the product to filter reviews"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
decimal min_rating? {
|
|
83
|
+
description = "Optional minimum rating to filter reviews"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
stack {
|
|
88
|
+
debug.log {
|
|
89
|
+
value = "Requester IP: " ~ $env.$remote_ip
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
conditional {
|
|
93
|
+
if ($input.min_rating != null) {
|
|
94
|
+
db.query "product_reviews" {
|
|
95
|
+
where = $db.product_reviews.product_id == $input.product_id && $db.product_reviews.rating >= $input.min_rating
|
|
96
|
+
sort = {product_reviews.review_date: "desc"}
|
|
97
|
+
return = {type: "list", paging: {page: 1, per_page: 25, totals: true}}
|
|
98
|
+
} as $reviews
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
else {
|
|
102
|
+
db.query "product_reviews" {
|
|
103
|
+
where = $db.product_reviews.product_id == $input.product_id
|
|
104
|
+
sort = {product_reviews.review_date: "desc"}
|
|
105
|
+
return = {type: "list", paging: {page: 1, per_page: 25, totals: true}}
|
|
106
|
+
} as $reviews
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
response = $reviews
|
|
112
|
+
|
|
113
|
+
history = 100
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## last_five_blog_posts
|
|
118
|
+
|
|
119
|
+
```xs
|
|
120
|
+
query "posts/recent" verb=GET {
|
|
121
|
+
description = "Retrieve the 5 most recent published blog posts, sorted by publication_date in descending order."
|
|
122
|
+
input {
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
stack {
|
|
126
|
+
db.query "blog_post" {
|
|
127
|
+
where = $db.blog_post.is_draft == false
|
|
128
|
+
sort = {blog_post.publication_date: "desc"}
|
|
129
|
+
return = {
|
|
130
|
+
type : "list"
|
|
131
|
+
paging: {page: 1, per_page: 5, metadata: false}
|
|
132
|
+
}
|
|
133
|
+
} as $recent_posts
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
response = $recent_posts
|
|
137
|
+
|
|
138
|
+
history = "inherit"
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## booking_reservation_system
|
|
143
|
+
|
|
144
|
+
```xs
|
|
145
|
+
query "create_booking" verb=POST {
|
|
146
|
+
description = "Create a new booking reservation with availability checking and conflict resolution"
|
|
147
|
+
auth = "user"
|
|
148
|
+
input {
|
|
149
|
+
int resource_id filters=min:1 {
|
|
150
|
+
description = "ID of the resource to book"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
text title filters=trim|min:1 {
|
|
154
|
+
description = "Title or purpose of the booking"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
text description? filters=trim {
|
|
158
|
+
description = "Detailed description of the booking purpose"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
timestamp start_time {
|
|
162
|
+
description = "When the booking should begin (ISO format)"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
timestamp end_time {
|
|
166
|
+
description = "When the booking should end (ISO format)"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
int attendee_count?=1 filters=min:1 {
|
|
170
|
+
description = "Number of people attending (default: 1)"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
email contact_email {
|
|
174
|
+
description = "Contact email for booking notifications"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
text contact_phone? filters=trim {
|
|
178
|
+
description = "Contact phone number"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
text special_requests? filters=trim {
|
|
182
|
+
description = "Any special requirements or requests"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
bool send_confirmation?=1 {
|
|
186
|
+
description = "Whether to send email confirmation (default: true)"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
stack {
|
|
191
|
+
precondition ($input.start_time < $input.end_time) {
|
|
192
|
+
description = "Validate booking time range is logical"
|
|
193
|
+
error_type = "inputerror"
|
|
194
|
+
error = "Start time must be before end time."
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
precondition ($input.start_time > now) {
|
|
198
|
+
description = "Validate booking is not in the past"
|
|
199
|
+
error_type = "inputerror"
|
|
200
|
+
error = "Cannot create bookings in the past."
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
db.query "resources" {
|
|
204
|
+
description = "Get the resource being booked"
|
|
205
|
+
where = $db.resources.id == $input.resource_id && $db.resources.is_active
|
|
206
|
+
return = {type: "list"}
|
|
207
|
+
} as $resource_data
|
|
208
|
+
|
|
209
|
+
precondition (($resource_data|count) > 0) {
|
|
210
|
+
description = "Validate resource exists and is active"
|
|
211
|
+
error_type = "inputerror"
|
|
212
|
+
error = "Resource not found or is not available for booking."
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
var $resource {
|
|
216
|
+
description = "Store the resource record"
|
|
217
|
+
value = $resource_data|first
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
precondition ($input.attendee_count <= $resource.capacity) {
|
|
221
|
+
description = "Validate attendee count does not exceed resource capacity"
|
|
222
|
+
error_type = "inputerror"
|
|
223
|
+
error = "Attendee count (" ~ $input.attendee_count ~ ") exceeds resource capacity (" ~ $resource.capacity ~ ")."
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
db.query "bookings" {
|
|
227
|
+
description = "Get all existing bookings for this resource to check conflicts"
|
|
228
|
+
where = $db.bookings.resource_id == $input.resource_id
|
|
229
|
+
return = {type: "list"}
|
|
230
|
+
} as $all_bookings
|
|
231
|
+
|
|
232
|
+
var $existing_bookings {
|
|
233
|
+
description = "Filter bookings to only confirmed or pending ones"
|
|
234
|
+
value = $all_bookings|filter:($this.status == "confirmed" || $this.status == "pending")
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
var $conflicts {
|
|
238
|
+
description = "Find conflicting bookings that overlap with requested time"
|
|
239
|
+
value = $existing_bookings|filter:(($this.start_time < $input.end_time) && ($this.end_time > $input.start_time))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
precondition (($conflicts|count) == 0) {
|
|
243
|
+
description = "Validate no time conflicts exist"
|
|
244
|
+
error_type = "inputerror"
|
|
245
|
+
error = "Time slot conflicts with existing booking. Please choose a different time."
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
var $booking_duration {
|
|
249
|
+
description = "Calculate booking duration in hours"
|
|
250
|
+
value = ($input.end_time - $input.start_time) / 3600
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
var $total_cost {
|
|
254
|
+
description = "Calculate total cost based on hourly rate and duration"
|
|
255
|
+
value = $booking_duration * $resource.hourly_rate
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
var $booking_reference {
|
|
259
|
+
description = "Generate unique booking reference code"
|
|
260
|
+
value = "BK-" ~ $input.resource_id ~ "-" ~ (now|to_timestamp) ~ "-" ~ ($auth.id|to_text)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
var $initial_status {
|
|
264
|
+
description = "Set initial status based on whether resource requires approval"
|
|
265
|
+
value = ($resource.requires_approval == true) ? "pending" : "confirmed"
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
db.add bookings {
|
|
269
|
+
description = "Create the new booking record"
|
|
270
|
+
data = {
|
|
271
|
+
resource_id : $input.resource_id
|
|
272
|
+
user_id : $auth.id
|
|
273
|
+
title : $input.title
|
|
274
|
+
description : $input.description
|
|
275
|
+
start_time : $input.start_time
|
|
276
|
+
end_time : $input.end_time
|
|
277
|
+
attendee_count : $input.attendee_count
|
|
278
|
+
contact_email : $input.contact_email
|
|
279
|
+
contact_phone : $input.contact_phone
|
|
280
|
+
status : $initial_status
|
|
281
|
+
total_cost : $total_cost
|
|
282
|
+
special_requests : $input.special_requests
|
|
283
|
+
booking_reference: $booking_reference
|
|
284
|
+
created_at : now
|
|
285
|
+
updated_at : now
|
|
286
|
+
}
|
|
287
|
+
} as $new_booking
|
|
288
|
+
|
|
289
|
+
conditional {
|
|
290
|
+
description = "Send confirmation email if requested"
|
|
291
|
+
if ($input.send_confirmation) {
|
|
292
|
+
var $email_subject {
|
|
293
|
+
description = "Create email subject line"
|
|
294
|
+
value = "Booking Confirmation - " ~ $resource.name ~ " (" ~ $booking_reference ~ ")"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
util.template_engine {
|
|
298
|
+
description = "Create email body using template"
|
|
299
|
+
value = """
|
|
300
|
+
Dear Customer,
|
|
301
|
+
|
|
302
|
+
Your booking has been {{ initial_status == 'confirmed' ? 'confirmed' : 'submitted for approval' }}.
|
|
303
|
+
|
|
304
|
+
Booking Details:
|
|
305
|
+
- Resource: {{ resource.name }}
|
|
306
|
+
- Date/Time: {{ input.start_time }} - {{ input.end_time }}
|
|
307
|
+
- Duration: {{ booking_duration }} hours
|
|
308
|
+
- Attendees: {{ input.attendee_count }}
|
|
309
|
+
- Total Cost: ${{ total_cost }}
|
|
310
|
+
- Reference: {{ booking_reference }}
|
|
311
|
+
|
|
312
|
+
Thank you for your booking!
|
|
313
|
+
"""
|
|
314
|
+
} as $email_body
|
|
315
|
+
|
|
316
|
+
util.send_email {
|
|
317
|
+
description = "Send booking confirmation email to user"
|
|
318
|
+
service_provider = "resend"
|
|
319
|
+
api_key = $env.secret_key
|
|
320
|
+
from = "no-reply@example.com"
|
|
321
|
+
to = $input.contact_email
|
|
322
|
+
subject = $email_subject
|
|
323
|
+
message = $email_body
|
|
324
|
+
} as $confirmation_email
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
var $booking_result {
|
|
329
|
+
description = "Prepare booking result summary"
|
|
330
|
+
value = {
|
|
331
|
+
booking_id: $new_booking.id
|
|
332
|
+
booking_reference: $booking_reference
|
|
333
|
+
resource_name: $resource.name
|
|
334
|
+
status: $initial_status
|
|
335
|
+
start_time: $input.start_time
|
|
336
|
+
end_time: $input.end_time
|
|
337
|
+
duration_hours: $booking_duration
|
|
338
|
+
total_cost: $total_cost
|
|
339
|
+
requires_approval: $resource.requires_approval
|
|
340
|
+
confirmation_sent: $input.send_confirmation
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
debug.log {
|
|
345
|
+
description = "Log successful booking creation"
|
|
346
|
+
value = {
|
|
347
|
+
action: "booking_created_successfully"
|
|
348
|
+
booking_id: $new_booking.id
|
|
349
|
+
booking_reference: $booking_reference
|
|
350
|
+
status: $initial_status
|
|
351
|
+
total_cost: $total_cost
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
response = $booking_result
|
|
357
|
+
|
|
358
|
+
history = "all"
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## audio_transcription_endpoint
|
|
363
|
+
|
|
364
|
+
```xs
|
|
365
|
+
query "transcribe/audio" verb=POST {
|
|
366
|
+
auth = "user"
|
|
367
|
+
description = "Upload an audio file to be transcribed using ElevenLabs API, store the file in cloud storage, and save transcription in the database."
|
|
368
|
+
|
|
369
|
+
input {
|
|
370
|
+
file audio {
|
|
371
|
+
description = "Audio file to be transcribed"
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
text ?language {
|
|
375
|
+
description = "Target language for transcription (optional, auto-detect if not provided)"
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
text access_level?=private filters=trim {
|
|
379
|
+
description = "Access level for the audio file (public or private)"
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
stack {
|
|
384
|
+
try_catch {
|
|
385
|
+
try {
|
|
386
|
+
conditional {
|
|
387
|
+
if ($input.access_level == "public") {
|
|
388
|
+
storage.create_attachment {
|
|
389
|
+
value = $input.audio
|
|
390
|
+
access = "public"
|
|
391
|
+
filename = $filename
|
|
392
|
+
description = "Store audio in cloud storage as public"
|
|
393
|
+
} as $audio_storage
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
else {
|
|
397
|
+
return {
|
|
398
|
+
value = "This function is built for public files. Please set access_level to public"
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
catch {
|
|
405
|
+
throw {
|
|
406
|
+
name = "Audio file issue"
|
|
407
|
+
value = ""
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
group {
|
|
413
|
+
description = "Handle audio upload and transcription process"
|
|
414
|
+
stack {
|
|
415
|
+
db.get "user" {
|
|
416
|
+
field_name = "id"
|
|
417
|
+
field_value = $auth.id
|
|
418
|
+
description = "Verify user exists"
|
|
419
|
+
} as $user
|
|
420
|
+
|
|
421
|
+
conditional {
|
|
422
|
+
if ($user == "") {
|
|
423
|
+
throw {
|
|
424
|
+
name = "inputerror"
|
|
425
|
+
value = "Invalid user ID provided."
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
var $allowed_audio_types {
|
|
431
|
+
value = ["audio/mpeg", "audio/mp3", "audio/wav", "audio/m4a", "audio/aac", "audio/ogg", "audio/flac"]
|
|
432
|
+
description = "List of allowed audio MIME types"
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
var $file_mime {
|
|
436
|
+
value = $audio_storage.mime
|
|
437
|
+
description = "MIME type of uploaded audio file"
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
var $file_size {
|
|
441
|
+
value = $audio_storage.size
|
|
442
|
+
description = "Size of uploaded audio file in bytes"
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
var $filename {
|
|
446
|
+
value = $audio_storage.name
|
|
447
|
+
description = "Original filename"
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
conditional {
|
|
451
|
+
if ($allowed_audio_types|contains:$file_mime) {
|
|
452
|
+
throw {
|
|
453
|
+
name = "inputerror"
|
|
454
|
+
value = "Invalid file type. Only audio files (MP3, WAV, M4A, AAC, OGG, FLAC) are allowed."
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
conditional {
|
|
460
|
+
if ($file_size > 26214400) {
|
|
461
|
+
throw {
|
|
462
|
+
name = "inputerror"
|
|
463
|
+
value = "File size too large. Maximum size is 25MB."
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
var $transcription_params {
|
|
469
|
+
value = {}
|
|
470
|
+
description = "Parameters for transcription request"
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
conditional {
|
|
474
|
+
if ($input.language != "") {
|
|
475
|
+
var.update $transcription_params {
|
|
476
|
+
value = $transcription_params|set:"language":$input.language
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
api.request {
|
|
482
|
+
url = "https://api.elevenlabs.io/v1/speech-to-text"
|
|
483
|
+
method = "POST"
|
|
484
|
+
params = {}|set:"file":$input.audio|set:"model_id":"scribe_v1"
|
|
485
|
+
headers = []|push:"xi-api-key: " ~ $env.elevenlabs_audio_key
|
|
486
|
+
timeout = 120
|
|
487
|
+
verify_host = false
|
|
488
|
+
verify_peer = false
|
|
489
|
+
description = "Send audio to ElevenLabs for transcription"
|
|
490
|
+
} as $transcription_response
|
|
491
|
+
|
|
492
|
+
var $transcript_text {
|
|
493
|
+
value = $transcription_response.response.result.text
|
|
494
|
+
description = "Extracted transcript text"
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
var $detected_language {
|
|
498
|
+
value = $transcription_response.response.result.language_code
|
|
499
|
+
description = "Language detected by ElevenLabs"
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
var $confidence {
|
|
503
|
+
value = $transcription_response.response.result.language_probability
|
|
504
|
+
description = "Transcription language confidence score"
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
db.add audio_file {
|
|
508
|
+
data = {
|
|
509
|
+
created_at : "now"
|
|
510
|
+
user : $auth.id
|
|
511
|
+
filename : $filename
|
|
512
|
+
file_type : $file_mime
|
|
513
|
+
file_size : $file_size
|
|
514
|
+
transcription: $transcript_text
|
|
515
|
+
status : "completed"
|
|
516
|
+
metadata : $audio_storage
|
|
517
|
+
audio_file : $audio_storage
|
|
518
|
+
}
|
|
519
|
+
} as $audio_file
|
|
520
|
+
|
|
521
|
+
var $result {
|
|
522
|
+
value = {
|
|
523
|
+
event: "audio_transcription_success",
|
|
524
|
+
audio_id: $audio_file.id,
|
|
525
|
+
user_id: $auth.id,
|
|
526
|
+
filename: $filename,
|
|
527
|
+
transcript_length: ($transcript_text|strlen)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
debug.log {
|
|
532
|
+
value = {
|
|
533
|
+
event: "audio_transcription_success",
|
|
534
|
+
audio_id: $audio_file.id,
|
|
535
|
+
user_id: $auth.id,
|
|
536
|
+
filename: $filename,
|
|
537
|
+
transcript_length: ($transcript_text|strlen)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
description = "Log successful transcription"
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
response = $result
|
|
547
|
+
|
|
548
|
+
history = false
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
## upload_image_endpoint
|
|
553
|
+
|
|
554
|
+
```xs
|
|
555
|
+
query "upload/image" verb=POST {
|
|
556
|
+
auth = "user"
|
|
557
|
+
input {
|
|
558
|
+
file? image
|
|
559
|
+
text access_level?=public filters=trim {
|
|
560
|
+
description = "Access level for the image (public or private)"
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
stack {
|
|
565
|
+
try_catch {
|
|
566
|
+
try {
|
|
567
|
+
conditional {
|
|
568
|
+
if ($input.access_level == "private") {
|
|
569
|
+
storage.create_image {
|
|
570
|
+
description = "Store image in cloud storage as private"
|
|
571
|
+
value = $input.image
|
|
572
|
+
access = "private"
|
|
573
|
+
filename = $filename
|
|
574
|
+
} as $image_storage
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
else {
|
|
578
|
+
storage.create_image {
|
|
579
|
+
description = "Store image in cloud storage as public"
|
|
580
|
+
value = $input.image
|
|
581
|
+
access = "public"
|
|
582
|
+
filename = $filename
|
|
583
|
+
} as $image_storage
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
catch {
|
|
589
|
+
throw {
|
|
590
|
+
name = "Invalid image"
|
|
591
|
+
value = $error.message
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
!debug.stop {
|
|
597
|
+
value = $image_storage
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
try_catch {
|
|
601
|
+
try {
|
|
602
|
+
var $allowed_types {
|
|
603
|
+
description = "List of allowed image MIME types"
|
|
604
|
+
value = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"]
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
var $file_mime {
|
|
608
|
+
description = "MIME type of uploaded file"
|
|
609
|
+
value = $image_storage.mime
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
var $file_size {
|
|
613
|
+
description = "Size of uploaded file in bytes"
|
|
614
|
+
value = $image_storage.size
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
var $filename {
|
|
618
|
+
description = "Original filename"
|
|
619
|
+
value = $image_storage.name
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
conditional {
|
|
623
|
+
if ($allowed_types|contains:$file_mime) {
|
|
624
|
+
throw {
|
|
625
|
+
name = "ValidationError"
|
|
626
|
+
value = "Invalid file type. Only JPEG, PNG, GIF, and WebP images are allowed."
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
conditional {
|
|
632
|
+
if ($file_size > 10485760) {
|
|
633
|
+
throw {
|
|
634
|
+
name = "ValidationError"
|
|
635
|
+
value = "File size too large. Maximum size is 10MB."
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
db.get "user" {
|
|
641
|
+
description = "Verify user exists"
|
|
642
|
+
field_name = "id"
|
|
643
|
+
field_value = $auth.id
|
|
644
|
+
} as $user
|
|
645
|
+
|
|
646
|
+
conditional {
|
|
647
|
+
if ($user == "") {
|
|
648
|
+
throw {
|
|
649
|
+
name = "ValidationError"
|
|
650
|
+
value = "Invalid user ID provided."
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
db.add images {
|
|
656
|
+
description = "Save image metadata to database"
|
|
657
|
+
data = {
|
|
658
|
+
created_at : "now"
|
|
659
|
+
user_id : $auth.id
|
|
660
|
+
filename : $filename
|
|
661
|
+
file_type : $file_mime
|
|
662
|
+
file_size : $file_size
|
|
663
|
+
image_url : $image_storage.path
|
|
664
|
+
access_level: $input.access_level
|
|
665
|
+
is_active : true
|
|
666
|
+
image_meta : $image_storage
|
|
667
|
+
}
|
|
668
|
+
} as $image_record
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
catch {
|
|
672
|
+
debug.log {
|
|
673
|
+
description = "Log upload failure"
|
|
674
|
+
value = "Image upload failed:"|concat:$error.message:" "
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
response = {
|
|
681
|
+
success : true
|
|
682
|
+
image_id : $image_record.id
|
|
683
|
+
image_url: $image_storage.path
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
history = "inherit"
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
## retrieve_active_user_profile
|
|
691
|
+
|
|
692
|
+
````xs
|
|
693
|
+
// API endpoint to retrieve active user profile details based on user_id
|
|
694
|
+
query "user_profile/{user_id}" verb=GET {
|
|
695
|
+
api_group = "profiles"
|
|
696
|
+
|
|
697
|
+
input {
|
|
698
|
+
// Unique identifier for the user
|
|
699
|
+
int user_id
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
stack {
|
|
703
|
+
// Retrieve active user profile details
|
|
704
|
+
db.query user {
|
|
705
|
+
where = $db.user.id == $input.user_id && $db.user.is_active
|
|
706
|
+
return = {type: "single"}
|
|
707
|
+
mock = {
|
|
708
|
+
"should return null for inactive": null
|
|
709
|
+
"should return active profile" : ```
|
|
710
|
+
{
|
|
711
|
+
"id": 7,
|
|
712
|
+
"user": 42,
|
|
713
|
+
"full_name": "Jane Doe",
|
|
714
|
+
"email": "jane@example.com",
|
|
715
|
+
"is_active": true
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
}
|
|
719
|
+
} as $profile
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
response = $profile
|
|
723
|
+
history = 10000
|
|
724
|
+
|
|
725
|
+
test "should return null for inactive" {
|
|
726
|
+
input = {user_id: 99}
|
|
727
|
+
|
|
728
|
+
expect.to_be_null ($response)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
test "should return active profile" {
|
|
732
|
+
input = {user_id: 42}
|
|
733
|
+
|
|
734
|
+
expect.to_equal ($response) {
|
|
735
|
+
value = {
|
|
736
|
+
id : 7
|
|
737
|
+
user : 42
|
|
738
|
+
full_name: "Jane Doe"
|
|
739
|
+
email : "jane@example.com"
|
|
740
|
+
is_active: true
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
````
|
|
746
|
+
|
|
747
|
+
## blog_posts_by_user
|
|
748
|
+
|
|
749
|
+
```xs
|
|
750
|
+
query "blog_posts_by_user/{user_id}" verb=GET {
|
|
751
|
+
description = "list all post by a given user and the number of comments and likes it received"
|
|
752
|
+
auth = "user"
|
|
753
|
+
input {
|
|
754
|
+
int user_id
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
stack {
|
|
758
|
+
db.query blog_post {
|
|
759
|
+
where = $db.blog_post.author_id == $input.user_id
|
|
760
|
+
sort = {blog_post.publication_date: "desc"}
|
|
761
|
+
return = {type: "list", paging: {page: 1, per_page: 25, totals: true}}
|
|
762
|
+
addon = [
|
|
763
|
+
{
|
|
764
|
+
name : "blog_post_likes"
|
|
765
|
+
input: {blog_post_id: $output.id}
|
|
766
|
+
as : "items.blog_post_likes"
|
|
767
|
+
}
|
|
768
|
+
{
|
|
769
|
+
name : "blog_post_comments"
|
|
770
|
+
input: {blog_post_id: $output.id}
|
|
771
|
+
as : "items.blog_post_comments"
|
|
772
|
+
}
|
|
773
|
+
]
|
|
774
|
+
} as $posts
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
response = $posts
|
|
778
|
+
|
|
779
|
+
history = "inherit"
|
|
780
|
+
}
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
## notification_system_endpoint
|
|
784
|
+
|
|
785
|
+
```xs
|
|
786
|
+
query "send_notification" verb=POST {
|
|
787
|
+
description = "Sends notifications to users via multiple channels (push, email, SMS). It validates input, checks user notification preferences, routes to appropriate external services, tracks delivery status, and logs all notification attempts with their outcomes."
|
|
788
|
+
input {
|
|
789
|
+
int user_id
|
|
790
|
+
text notification_type {
|
|
791
|
+
description = "Type of notification: push, email, sms"
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
text title filters=trim {
|
|
795
|
+
description = "Notification title"
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
text message filters=trim {
|
|
799
|
+
description = "Notification message content"
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
json data {
|
|
803
|
+
description = "Additional notification data"
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
bool force? {
|
|
807
|
+
description = "Force send even if user has disabled this notification type"
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
stack {
|
|
812
|
+
debug.log {
|
|
813
|
+
description = "Log notification attempt start"
|
|
814
|
+
value = "Starting notification send for user " ~ $input.user_id
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
precondition ($input.notification_type == "push" || $input.notification_type == "email" || $input.notification_type == "sms") {
|
|
818
|
+
description = "Validate that notification type is one of the three supported types"
|
|
819
|
+
error_type = "inputerror"
|
|
820
|
+
error = "Invalid notification type. Must be: push, email, or sms"
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
precondition (($input.title|strlen) > 0) {
|
|
824
|
+
description = "Ensure the notification title is not empty"
|
|
825
|
+
error_type = "inputerror"
|
|
826
|
+
error = "Notification title is required"
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
db.query "notification_preference" {
|
|
830
|
+
description = "Check user notification preferences"
|
|
831
|
+
where = $db.notification_preference.user_id == $input.user_id && $db.notification_preference.notification_type == $input.notification_type
|
|
832
|
+
return = {type: "list"}
|
|
833
|
+
} as $user_preferences
|
|
834
|
+
|
|
835
|
+
conditional {
|
|
836
|
+
description = "If no preferences exist, create default preferences with notifications enabled. Otherwise, get the user's enabled status from existing preferences"
|
|
837
|
+
if (($user_preferences|count) == 0) {
|
|
838
|
+
db.add notification_preference {
|
|
839
|
+
description = "Create default notification preference"
|
|
840
|
+
data = {
|
|
841
|
+
user_id : $input.user_id
|
|
842
|
+
notification_type: $input.notification_type
|
|
843
|
+
enabled : true
|
|
844
|
+
preferences : "{}"
|
|
845
|
+
}
|
|
846
|
+
} as $new_preference
|
|
847
|
+
|
|
848
|
+
var $user_enabled {
|
|
849
|
+
value = true
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
elseif ($input.notification_type == "asdfasdf") {
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
else {
|
|
857
|
+
var $user_enabled {
|
|
858
|
+
value = ($user_preferences|first).enabled
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
conditional {
|
|
864
|
+
description = "Set up different API calls based on notification type"
|
|
865
|
+
if ($input.notification_type == "push") {
|
|
866
|
+
api.request {
|
|
867
|
+
description = "Sample API call"
|
|
868
|
+
url = "https://mandrillapp.com/api/1.0/messages/send"
|
|
869
|
+
method = "POST"
|
|
870
|
+
params = {}|set:"key":$env.your_api_key|set:"message":({}|set:"from_email":"you@yourdomain.com"|set:"from_name":"Your Name"|set:"subject":"Test Transactional Email"|set:"text":"Hello, this is a transactional email!"|set:"to":([]|push:({}|set:"email":"recipient@example.com"|set:"name":"Recipient Name"|set:"type":"to")))
|
|
871
|
+
headers = []|push:"Content-Type: application/json"
|
|
872
|
+
} as $api_endpoint
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
elseif ($input.notification_type == "email") {
|
|
876
|
+
api.request {
|
|
877
|
+
description = "Sample API call"
|
|
878
|
+
url = "https://mandrillapp.com/api/1.0/messages/send"
|
|
879
|
+
method = "POST"
|
|
880
|
+
params = {}|set:"key":$env.your_api_key|set:"message":({}|set:"from_email":"you@yourdomain.com"|set:"from_name":"Your Name"|set:"subject":"Test Transactional Email"|set:"text":"Hello, this is a transactional email!"|set:"to":([]|push:({}|set:"email":"recipient@example.com"|set:"name":"Recipient Name"|set:"type":"to")))
|
|
881
|
+
headers = []|push:"Content-Type: application/json"
|
|
882
|
+
} as $api_endpoint
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
elseif ($input.notification_type == "sms") {
|
|
886
|
+
api.request {
|
|
887
|
+
description = "Sample API call"
|
|
888
|
+
url = "https://api.twilio.com/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Messages"
|
|
889
|
+
method = "POST"
|
|
890
|
+
params = {}|set:"To":" 1234567890"|set:"From":" 1987654321"|set:"Body":"Hello from Twilio!"
|
|
891
|
+
headers = []|push:("Authorization: Basic %s"|sprintf:("ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:your_auth_token"|base64_encode))
|
|
892
|
+
} as $api_endpoint
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
conditional {
|
|
897
|
+
description = "Skip user if not enabled AND force send is false"
|
|
898
|
+
if ($user_enabled == false && $input.force == false) {
|
|
899
|
+
var $status_result {
|
|
900
|
+
value = "skipped"
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
var $external_ref {
|
|
904
|
+
value = ""
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
debug.log {
|
|
908
|
+
value = "Skipped user"
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
conditional {
|
|
914
|
+
description = "Process the API response and set status"
|
|
915
|
+
if ($api_response.status_code >= 200 && $api_response.status_code < 300) {
|
|
916
|
+
var $status_result {
|
|
917
|
+
value = "sent"
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
var $external_ref {
|
|
921
|
+
value = $api_response.body.id
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
elseif ($status_result == "skipped") {
|
|
926
|
+
debug.log {
|
|
927
|
+
value = "Status already set above, do nothing"
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
else {
|
|
932
|
+
var $status_result {
|
|
933
|
+
value = "failed"
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
var $external_ref {
|
|
937
|
+
value = ""
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
db.add notification_log {
|
|
943
|
+
description = "Log the notification attempt to the database"
|
|
944
|
+
data = {
|
|
945
|
+
created_at : "now"
|
|
946
|
+
user_id : $input.user_id
|
|
947
|
+
notification_type: $input.notification_type
|
|
948
|
+
title : $input.title
|
|
949
|
+
message : $input.message
|
|
950
|
+
response_data : $api_endpoint
|
|
951
|
+
}
|
|
952
|
+
} as $notification_log
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
response = null
|
|
956
|
+
|
|
957
|
+
history = 1000
|
|
958
|
+
}
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
## products_search_endpoint
|
|
962
|
+
|
|
963
|
+
Searches products with optional filters and pagination. Notice how the `category_id` filter is optional and the search term will be ignored if not provided (thanks to the operator `==?`).
|
|
964
|
+
|
|
965
|
+
```xs
|
|
966
|
+
query "products/search" verb=GET {
|
|
967
|
+
input {
|
|
968
|
+
text search_term? filters=trim {
|
|
969
|
+
description = "Search term to match against product name and description"
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
int category_id? filters=min:0 {
|
|
973
|
+
description = "Optional category ID to filter products"
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
int page?=1 filters=min:1 {
|
|
977
|
+
description = "Page number for pagination (starts at 1)"
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
int per_page?=20 filters=min:1|max:100 {
|
|
981
|
+
description = "Number of items per page (max 100)"
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
stack {
|
|
986
|
+
db.query "product" {
|
|
987
|
+
description = "Search products with filters and pagination"
|
|
988
|
+
where = $db.product.category_id ==? $input.category_id && ($db.product.name includes? $input.search_term || $db.product.description includes? $input.search_term)
|
|
989
|
+
sort = {product.price: "asc"}
|
|
990
|
+
return = {type: "list"}
|
|
991
|
+
} as $products
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
response = $products
|
|
995
|
+
|
|
996
|
+
history = "inherit"
|
|
997
|
+
}
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
## signup_endpoint
|
|
1001
|
+
|
|
1002
|
+
```xs
|
|
1003
|
+
query "auth/signup" verb=POST {
|
|
1004
|
+
description = "Signup and retrieve an authentication token"
|
|
1005
|
+
input {
|
|
1006
|
+
text name? {
|
|
1007
|
+
description = "User's name (optional)"
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
email email? {
|
|
1011
|
+
description = "User's email address (required for signup)"
|
|
1012
|
+
sensitive = true
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
text password? {
|
|
1016
|
+
description = "User's password (required for signup)"
|
|
1017
|
+
sensitive = true
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
stack {
|
|
1022
|
+
db.get "user" {
|
|
1023
|
+
description = "Check if a user with this email already exists"
|
|
1024
|
+
field_name = "email"
|
|
1025
|
+
field_value = $input.email
|
|
1026
|
+
} as $user
|
|
1027
|
+
|
|
1028
|
+
precondition ($user == null) {
|
|
1029
|
+
description = "Prevent duplicate signup with the same email"
|
|
1030
|
+
error_type = "accessdenied"
|
|
1031
|
+
error = "This account is already in use."
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
db.add user {
|
|
1035
|
+
description = "Create a new user record with provided details"
|
|
1036
|
+
data = {
|
|
1037
|
+
created_at: "now"
|
|
1038
|
+
name : $input.name
|
|
1039
|
+
email : $input.email
|
|
1040
|
+
password : $input.password
|
|
1041
|
+
}
|
|
1042
|
+
} as $user
|
|
1043
|
+
|
|
1044
|
+
security.create_auth_token {
|
|
1045
|
+
description = "Generate an authentication token for the new user"
|
|
1046
|
+
table = "user"
|
|
1047
|
+
extras = {}
|
|
1048
|
+
expiration = 86400
|
|
1049
|
+
id = $user.id
|
|
1050
|
+
} as $authToken
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
response = {authToken: $authToken}
|
|
1054
|
+
|
|
1055
|
+
history = 1000
|
|
1056
|
+
}
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
## search_users_endpoint
|
|
1060
|
+
|
|
1061
|
+
```xs
|
|
1062
|
+
query "users" verb=GET {
|
|
1063
|
+
auth = "user"
|
|
1064
|
+
input {
|
|
1065
|
+
text filter
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
stack {
|
|
1069
|
+
db.query "user" {
|
|
1070
|
+
description = "Search users"
|
|
1071
|
+
where = $db.user.name includes $input.filter
|
|
1072
|
+
sort = {user.name: "asc"}
|
|
1073
|
+
return = {type: "list"}
|
|
1074
|
+
} as $users
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
response = $users
|
|
1078
|
+
|
|
1079
|
+
history = "inherit"
|
|
1080
|
+
}
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
```xs
|
|
1084
|
+
query "webhook_receiver" verb=POST {
|
|
1085
|
+
input {
|
|
1086
|
+
text webhook_id
|
|
1087
|
+
text signature?
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
stack {
|
|
1091
|
+
util.get_input {
|
|
1092
|
+
encoding = "json"
|
|
1093
|
+
} as $payload_data
|
|
1094
|
+
|
|
1095
|
+
conditional {
|
|
1096
|
+
if ($payload_data.body.event_type == "user.created") {
|
|
1097
|
+
db.add webhook_logs {
|
|
1098
|
+
data = {
|
|
1099
|
+
webhook_id : $input.webhook_id
|
|
1100
|
+
event_type : "user.created"
|
|
1101
|
+
status : "processed"
|
|
1102
|
+
payload : $payload_data
|
|
1103
|
+
signature : $input.signature
|
|
1104
|
+
user_id : $payload_data.body.user.id
|
|
1105
|
+
received_at : now
|
|
1106
|
+
processed_at: now
|
|
1107
|
+
retry_count : 0
|
|
1108
|
+
max_retries : 3
|
|
1109
|
+
}
|
|
1110
|
+
} as $webhook_record
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
elseif ($payload_data.body.event_type == "payment.completed") {
|
|
1114
|
+
db.add webhook_logs {
|
|
1115
|
+
data = {
|
|
1116
|
+
webhook_id : $input.webhook_id
|
|
1117
|
+
event_type : "payment.completed"
|
|
1118
|
+
status : "processed"
|
|
1119
|
+
payload : $payload_data
|
|
1120
|
+
signature : $input.signature
|
|
1121
|
+
payment_id : $payload_data.payment.id
|
|
1122
|
+
amount : $payload_data.payment.amount
|
|
1123
|
+
received_at : now
|
|
1124
|
+
processed_at: now
|
|
1125
|
+
retry_count : 0
|
|
1126
|
+
max_retries : 3
|
|
1127
|
+
}
|
|
1128
|
+
} as $webhook_record
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
else {
|
|
1132
|
+
db.add webhook_logs {
|
|
1133
|
+
data = {
|
|
1134
|
+
webhook_id : $input.webhook_id
|
|
1135
|
+
event_type : $payload_data.event_type
|
|
1136
|
+
status : "processed"
|
|
1137
|
+
payload : $payload_data
|
|
1138
|
+
signature : $input.signature
|
|
1139
|
+
received_at : now
|
|
1140
|
+
processed_at: now
|
|
1141
|
+
retry_count : 0
|
|
1142
|
+
max_retries : 3
|
|
1143
|
+
}
|
|
1144
|
+
} as $webhook_record
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
response = {
|
|
1150
|
+
webhook_processed: true
|
|
1151
|
+
message : "Webhook processed successfully"
|
|
1152
|
+
webhook_id : $input.webhook_id
|
|
1153
|
+
event_type : $payload_data.body.event_type
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
history = 100
|
|
1157
|
+
}
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
```
|
|
1161
|
+
query "list_orders" verb=GET {
|
|
1162
|
+
description = "Display all the order in a html table view"
|
|
1163
|
+
input {
|
|
1164
|
+
int year?=2025
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
stack {
|
|
1168
|
+
var $start_of_year {
|
|
1169
|
+
description = "Start of the year for filtering"
|
|
1170
|
+
value = $input.year ~ "-01-01T00:00:00.000-08:00"
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
var $end_of_year {
|
|
1174
|
+
description = "End of the year for filtering (exclusive)"
|
|
1175
|
+
value = ($input.year + 1) ~ "-01-01T00:00:00.000-08:00"
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
db.query orders {
|
|
1179
|
+
where = $db.orders.order_date >= $start_of_year && $db.orders.order_date < $end_of_year
|
|
1180
|
+
return = {type: "list"}
|
|
1181
|
+
addon = [
|
|
1182
|
+
{
|
|
1183
|
+
name : "customer"
|
|
1184
|
+
input: {customer_id: $output.customer_id}
|
|
1185
|
+
as : "_customer"
|
|
1186
|
+
}
|
|
1187
|
+
]
|
|
1188
|
+
} as $orders
|
|
1189
|
+
|
|
1190
|
+
util.template_engine {
|
|
1191
|
+
description = "Initialize HTML output with table structure"
|
|
1192
|
+
value = """
|
|
1193
|
+
<!DOCTYPE html>
|
|
1194
|
+
<html lang="en">
|
|
1195
|
+
<head>
|
|
1196
|
+
<meta charset="UTF-8">
|
|
1197
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1198
|
+
<title>Order List</title>
|
|
1199
|
+
<!-- Bootstrap CSS CDN -->
|
|
1200
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
|
1201
|
+
</head>
|
|
1202
|
+
<body>
|
|
1203
|
+
<div class="container mt-5">
|
|
1204
|
+
<h1 class="mb-4">Customer Orders</h1>
|
|
1205
|
+
|
|
1206
|
+
{% if $var.orders is defined and $var.orders|length > 0 %}
|
|
1207
|
+
<table class="table table-striped table-bordered table-hover">
|
|
1208
|
+
<thead class="table-dark">
|
|
1209
|
+
<tr>
|
|
1210
|
+
<th scope="col">Order ID</th>
|
|
1211
|
+
<th scope="col">Customer Address</th>
|
|
1212
|
+
<th scope="col">Product Name</th>
|
|
1213
|
+
<th scope="col">Price</th>
|
|
1214
|
+
<th scope="col">Status</th>
|
|
1215
|
+
</tr>
|
|
1216
|
+
</thead>
|
|
1217
|
+
<tbody>
|
|
1218
|
+
{% for order in $var.orders %}
|
|
1219
|
+
<tr>
|
|
1220
|
+
<td>{{ order.id }}</td>
|
|
1221
|
+
<td>{{ order._customer.street_address }}{% if order._customer.city %}, {{ order._customer.city }}{% endif %}{% if order._customer.zip_code %}, {{ order._customer.zip_code }}{% endif %}</td>
|
|
1222
|
+
<td>{{ order.product_name }}</td>
|
|
1223
|
+
<td>${{ order.order_total|number_format(2) }}</td>
|
|
1224
|
+
<td><span class="badge bg-{% if order.status == 'completed' %}success{% elseif order.status == 'pending' %}warning{% else %}danger{% endif %}">{{ order.status|capitalize }}</span></td>
|
|
1225
|
+
|
|
1226
|
+
</tr>
|
|
1227
|
+
{% endfor %}
|
|
1228
|
+
</tbody>
|
|
1229
|
+
</table>
|
|
1230
|
+
{% else %}
|
|
1231
|
+
<div class="alert alert-info" role="alert">
|
|
1232
|
+
No orders found.
|
|
1233
|
+
</div>
|
|
1234
|
+
{% endif %}
|
|
1235
|
+
|
|
1236
|
+
</div>
|
|
1237
|
+
|
|
1238
|
+
<!-- Bootstrap JS and Popper.js CDN (optional, for interactive components) -->
|
|
1239
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
|
1240
|
+
</body>
|
|
1241
|
+
</html>
|
|
1242
|
+
"""
|
|
1243
|
+
} as $html_output
|
|
1244
|
+
|
|
1245
|
+
util.set_header {
|
|
1246
|
+
value = "Content-Type: text/html; charset=utf-8"
|
|
1247
|
+
duplicates = "replace"
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
response = $html_output
|
|
1252
|
+
|
|
1253
|
+
history = "inherit"
|
|
1254
|
+
}
|
|
1255
|
+
```
|