feat: big-suite terraform example + provider nodejs zip fix

- examples/big-suite: E-Commerce 10 functions, 6 layers, real depends_on
- provider: nodejs packages now wrapped in ESM zip (buildJSDeployZip)
- provider: loadPackageLiteral reads raw file, nodejs zip via source_dir
- all 10 functions verified working: python/node/ruby/go runtimes
This commit is contained in:
Naeel
2026-04-20 11:38:45 +03:00
parent 4837d42ba1
commit 01df1498d6
22 changed files with 1033 additions and 5 deletions
+3
View File
@@ -0,0 +1,3 @@
module github.com/user/fn
go 1.23
@@ -0,0 +1,98 @@
package main
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"time"
)
type OrderItem struct {
ProductID string `json:"product_id"`
Name string `json:"name"`
Price float64 `json:"price"`
Qty int `json:"qty"`
}
type OrderRequest struct {
UserID string `json:"user_id"`
Items []OrderItem `json:"items"`
PayToken string `json:"pay_token"`
Address string `json:"address"`
}
type OrderResponse struct {
Status string `json:"status"`
OrderID string `json:"order_id"`
UserID string `json:"user_id"`
Items []OrderItem `json:"items"`
Total float64 `json:"total"`
CreatedAt string `json:"created_at"`
EstDays int `json:"estimated_delivery_days"`
}
func generateOrderID() string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
r := rand.New(rand.NewSource(time.Now().UnixNano()))
b := make([]byte, 10)
for i := range b {
b[i] = chars[r.Intn(len(chars))]
}
return "ORD-" + string(b)
}
func calcTotal(items []OrderItem) float64 {
total := 0.0
for _, item := range items {
total += item.Price * float64(item.Qty)
}
// Округление до 2 знаков
return float64(int(total*100)) / 100
}
func Handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintln(w, `{"error":"method not allowed"}`)
return
}
var req OrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, `{"error":"invalid JSON: %s"}`, err.Error())
return
}
if req.UserID == "" {
w.WriteHeader(http.StatusUnprocessableEntity)
fmt.Fprintln(w, `{"error":"user_id required"}`)
return
}
if len(req.Items) == 0 {
w.WriteHeader(http.StatusUnprocessableEntity)
fmt.Fprintln(w, `{"error":"items cannot be empty"}`)
return
}
if req.PayToken == "" {
w.WriteHeader(http.StatusUnprocessableEntity)
fmt.Fprintln(w, `{"error":"pay_token required"}`)
return
}
resp := OrderResponse{
Status: "created",
OrderID: generateOrderID(),
UserID: req.UserID,
Items: req.Items,
Total: calcTotal(req.Items),
CreatedAt: time.Now().UTC().Format(time.RFC3339),
EstDays: 3 + rand.Intn(5),
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(resp)
}
+3
View File
@@ -0,0 +1,3 @@
module github.com/user/fn
go 1.23
@@ -0,0 +1,68 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
)
// Симулирует агрегацию данных за день (в реальности — запрос к БД)
type DailyStat struct {
Date string `json:"date"`
Orders int `json:"orders"`
Revenue float64 `json:"revenue"`
AvgOrder float64 `json:"avg_order_value"`
NewUsers int `json:"new_users"`
TopCategory string `json:"top_category"`
}
func generateDailyStats(days int) []DailyStat {
stats := make([]DailyStat, days)
base := time.Now().UTC()
revenues := []float64{4821.50, 3920.00, 6100.75, 5200.10, 7330.40, 4100.00, 8950.30}
orders := []int{38, 31, 49, 42, 58, 33, 72}
categories := []string{"electronics", "furniture", "electronics", "stationery", "electronics", "furniture", "electronics"}
for i := 0; i < days; i++ {
day := base.AddDate(0, 0, -(days - 1 - i))
rev := revenues[i%len(revenues)]
ord := orders[i%len(orders)]
stats[i] = DailyStat{
Date: day.Format("2006-01-02"),
Orders: ord,
Revenue: rev,
AvgOrder: float64(int(rev/float64(ord)*100)) / 100,
NewUsers: 5 + i*2,
TopCategory: categories[i%len(categories)],
}
}
return stats
}
func Handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
days := 7
stats := generateDailyStats(days)
totalRevenue := 0.0
totalOrders := 0
for _, s := range stats {
totalRevenue += s.Revenue
totalOrders += s.Orders
}
report := map[string]interface{}{
"status": "ok",
"generated_at": time.Now().UTC().Format(time.RFC3339),
"period_days": days,
"total_revenue": fmt.Sprintf("%.2f", totalRevenue),
"total_orders": totalOrders,
"avg_daily_rev": fmt.Sprintf("%.2f", totalRevenue/float64(days)),
"daily_breakdown": stats,
}
json.NewEncoder(w).Encode(report)
}
@@ -0,0 +1,20 @@
exports.handler = async (ctx) => {
const start = Date.now();
return {
body: JSON.stringify({
status: "ok",
service: "ecom-platform",
version: "1.0.0",
uptime_check: "pass",
checks: {
runtime: "ok",
memory_mb: process.memoryUsage ? Math.round(process.memoryUsage().heapUsed / 1024 / 1024) : null,
node_version: process.version || "unknown",
},
timestamp: new Date().toISOString(),
response_ms: Date.now() - start,
}),
headers: { "Content-Type": "application/json" },
};
};
@@ -0,0 +1,70 @@
// Валидирует платёжные данные перед созданием заказа
exports.handler = async (ctx) => {
const req = ctx.request;
if (!req.body) {
return {
status: 400,
body: JSON.stringify({ error: "request body required" }),
headers: { "Content-Type": "application/json" },
};
}
let data;
try {
data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
} catch {
return {
status: 400,
body: JSON.stringify({ error: "invalid JSON" }),
headers: { "Content-Type": "application/json" },
};
}
const errors = [];
// card_number: 16 цифр
const card = String(data.card_number || "").replace(/\s/g, "");
if (!/^\d{16}$/.test(card)) errors.push("card_number must be 16 digits");
// cvv: 3 цифры
const cvv = String(data.cvv || "");
if (!/^\d{3}$/.test(cvv)) errors.push("cvv must be 3 digits");
// expiry: MM/YY
const expiry = String(data.expiry || "");
if (!/^(0[1-9]|1[0-2])\/\d{2}$/.test(expiry)) errors.push("expiry must be MM/YY");
else {
const [mm, yy] = expiry.split("/").map(Number);
const now = new Date();
const exp = new Date(2000 + yy, mm - 1);
if (exp < now) errors.push("card is expired");
}
// amount
const amount = Number(data.amount);
if (!amount || amount <= 0) errors.push("amount must be positive number");
if (amount > 10000) errors.push("amount exceeds limit 10000");
if (errors.length > 0) {
return {
status: 422,
body: JSON.stringify({ valid: false, errors }),
headers: { "Content-Type": "application/json" },
};
}
// Маскируем карту в ответе
const maskedCard = `****-****-****-${card.slice(-4)}`;
return {
body: JSON.stringify({
valid: true,
masked_card: maskedCard,
amount,
currency: data.currency || "USD",
token: `pay_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
}),
headers: { "Content-Type": "application/json" },
};
};
@@ -0,0 +1,62 @@
// Rate-limiter: проверяет X-Client-ID, считает запросы в памяти пода
// В реальном проекте — Redis. Здесь: in-memory для демонстрации.
const counters = {};
const WINDOW_MS = 60_000;
const LIMIT = 100;
exports.handler = async (ctx) => {
const req = ctx.request;
const clientId = (req.headers && req.headers["x-client-id"]) || "anonymous";
const now = Date.now();
if (!counters[clientId]) {
counters[clientId] = { count: 0, windowStart: now };
}
const c = counters[clientId];
if (now - c.windowStart > WINDOW_MS) {
c.count = 0;
c.windowStart = now;
}
c.count++;
const allowed = c.count <= LIMIT;
const remaining = Math.max(0, LIMIT - c.count);
const resetAt = new Date(c.windowStart + WINDOW_MS).toISOString();
if (!allowed) {
return {
status: 429,
body: JSON.stringify({
error: "rate limit exceeded",
client_id: clientId,
limit: LIMIT,
reset_at: resetAt,
}),
headers: {
"Content-Type": "application/json",
"X-RateLimit-Limit": String(LIMIT),
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": resetAt,
},
};
}
return {
body: JSON.stringify({
allowed: true,
client_id: clientId,
requests_in_window: c.count,
remaining,
limit: LIMIT,
reset_at: resetAt,
}),
headers: {
"Content-Type": "application/json",
"X-RateLimit-Limit": String(LIMIT),
"X-RateLimit-Remaining": String(remaining),
},
};
};
@@ -0,0 +1,44 @@
import hashlib
import hmac
import json
import time
from flask import request
_SECRET = b"ecom-secret-2026"
_USERS = {
"tok_alice_001": {"sub": "user-1", "name": "Alice", "roles": ["buyer"]},
"tok_bob_002": {"sub": "user-2", "name": "Bob", "roles": ["buyer", "seller"]},
"tok_admin_999": {"sub": "user-0", "name": "Admin", "roles": ["admin"]},
}
def _verify_token(token: str) -> dict | None:
if token in _USERS:
return _USERS[token]
return None
def main():
token = request.headers.get("x-bearer-token", "").strip()
if not token:
auth_header = request.headers.get("authorization", "")
token = auth_header.removeprefix("Bearer ").strip()
if not token:
return {"status": 401, "body": json.dumps({"error": "missing token"}),
"headers": {"Content-Type": "application/json"}}
user = _verify_token(token)
if not user:
return {"status": 403, "body": json.dumps({"error": "invalid token"}),
"headers": {"Content-Type": "application/json"}}
payload = {
"authenticated": True,
"sub": user["sub"],
"name": user["name"],
"roles": user["roles"],
"checked_at": int(time.time()),
}
return json.dumps(payload)
@@ -0,0 +1,70 @@
import json
import time
from flask import request
# In-memory корзина (живёт пока жив под poolmgr)
_CARTS: dict = {}
def _get_cart(user_id: str) -> dict:
if user_id not in _CARTS:
_CARTS[user_id] = {"user_id": user_id, "items": [], "created_at": int(time.time())}
return _CARTS[user_id]
def _cart_total(cart: dict) -> float:
return round(sum(i["price"] * i["qty"] for i in cart["items"]), 2)
def main():
method = request.method.upper()
user_id = request.headers.get("x-user-id", "user-1")
body = {}
if request.data:
try:
body = json.loads(request.data)
except Exception:
pass
cart = _get_cart(user_id)
if method == "GET":
return json.dumps({
"status": "ok",
"cart": cart,
"total": _cart_total(cart),
"item_count": len(cart["items"]),
})
if method == "POST":
# Добавить товар: {"product_id": "p-001", "name": "...", "price": 29.99, "qty": 2}
required = ["product_id", "name", "price", "qty"]
if not all(k in body for k in required):
return {"status": 400,
"body": json.dumps({"error": "missing fields", "required": required}),
"headers": {"Content-Type": "application/json"}}
# Если товар уже в корзине — увеличиваем qty
for item in cart["items"]:
if item["product_id"] == body["product_id"]:
item["qty"] += int(body["qty"])
return json.dumps({"status": "ok", "action": "updated", "cart": cart,
"total": _cart_total(cart)})
cart["items"].append({
"product_id": body["product_id"],
"name": body["name"],
"price": float(body["price"]),
"qty": int(body["qty"]),
})
return json.dumps({"status": "ok", "action": "added", "cart": cart,
"total": _cart_total(cart)})
if method == "DELETE":
product_id = body.get("product_id", "")
cart["items"] = [i for i in cart["items"] if i["product_id"] != product_id]
return json.dumps({"status": "ok", "action": "removed", "cart": cart,
"total": _cart_total(cart)})
return {"status": 405, "body": json.dumps({"error": "method not allowed"}),
"headers": {"Content-Type": "application/json"}}
@@ -0,0 +1,48 @@
import json
from flask import request
_DB = {
"user-1": {"id": "user-1", "name": "Alice", "email": "alice@example.com", "tier": "gold", "orders": 42},
"user-2": {"id": "user-2", "name": "Bob", "email": "bob@example.com", "tier": "silver", "orders": 11},
"user-0": {"id": "user-0", "name": "Admin", "email": "admin@example.com", "tier": "admin", "orders": 0},
}
def main():
# Path variables не передаются в Fission функцию — используем query param ?id=
user_id = request.args.get("id", "")
if not user_id or user_id not in _DB:
return {
"status": 404,
"body": json.dumps({"error": "user not found", "id": user_id}),
"headers": {"Content-Type": "application/json"},
}
user = dict(_DB[user_id])
user.pop("email", None)
return json.dumps({"status": "ok", "user": user})
# Достаём id из query string или пути
user_id = ""
if hasattr(context, "request"):
path = getattr(context.request, "url", "") or ""
# /ecom/users/user-1 → user-1
parts = [p for p in path.split("/") if p]
if parts:
user_id = parts[-1]
qs = getattr(context.request, "query", {}) or {}
if not user_id and "id" in qs:
user_id = qs["id"]
if not user_id or user_id not in _DB:
return {
"status": 404,
"body": json.dumps({"error": "user not found", "id": user_id}),
"headers": {"Content-Type": "application/json"},
}
user = dict(_DB[user_id])
# Не возвращаем email в публичном ответе
user.pop("email", None)
return json.dumps({"status": "ok", "user": user})
@@ -0,0 +1,40 @@
# frozen_string_literal: true
require "json"
CHANNELS = %w[email sms push].freeze
def handler
# В реальности: читаем body запроса, определяем канал и шлём уведомление
# Здесь: симуляция отправки по всем каналам
order_id = "ORD-DEMO"
user_name = "Alice"
amount = 129.99
results = CHANNELS.map do |ch|
# Симуляция: email — всегда ок, sms — 95%, push — 90%
success = case ch
when "email" then true
when "sms" then rand(100) < 95
when "push" then rand(100) < 90
end
{
channel: ch,
status: success ? "sent" : "failed",
message: "Order #{order_id} confirmed — total $#{amount}",
recipient: "#{user_name.downcase}@notify",
}
end
sent = results.count { |r| r[:status] == "sent" }
total = results.length
{
status: "ok",
order_id: order_id,
sent: sent,
failed: total - sent,
channels: results,
sent_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
}.to_json
end
@@ -0,0 +1,29 @@
# frozen_string_literal: true
require "json"
PRODUCTS = [
{ id: "p-001", name: "Laptop Pro 15", price: 1299.99, category: "electronics", stock: 14 },
{ id: "p-002", name: "Wireless Mouse", price: 29.99, category: "electronics", stock: 203 },
{ id: "p-003", name: "Standing Desk", price: 549.00, category: "furniture", stock: 7 },
{ id: "p-004", name: "USB-C Hub 7-in-1", price: 49.99, category: "electronics", stock: 88 },
{ id: "p-005", name: "Ergonomic Chair", price: 399.00, category: "furniture", stock: 3 },
{ id: "p-006", name: "Monitor 4K 27\"", price: 699.00, category: "electronics", stock: 22 },
{ id: "p-007", name: "Notebook A5", price: 4.99, category: "stationery", stock: 500 },
{ id: "p-008", name: "Pen Set 12pc", price: 9.99, category: "stationery", stock: 320 },
].freeze
def handler
# Простая фильтрация по category (query string не доступен в v1 Perl-style,
# но для Ruby v3 env возвращаем весь каталог)
total = PRODUCTS.length
in_stock = PRODUCTS.count { |p| p[:stock] > 0 }
by_cat = PRODUCTS.group_by { |p| p[:category] }.transform_values(&:length)
{
status: "ok",
total: total,
in_stock: in_stock,
by_category: by_cat,
products: PRODUCTS,
}.to_json
end