انتشار یک اپیآی سفارشی
هدف
به سامانهٔ یک شرکت دیگر تنها یک نشانی برای فراخوانی بدهید — POST /api/par/v1/invoices/{id}/approve —
و کاری کنید که همان نشانی، پس از تغییر نام یک ستون هم دقیقاً مثل قبل کار کند.
چرا این وجود دارد
پارسور از پیش برای جدولهای شما یک اپیآی دارد. آن اپیآی از فرادادههای جدول ساخته میشود،
یعنی هر بار که جدول تغییر کند شکل آن هم تغییر میکند. این دقیقاً همان چیزی است که درون پلتفرم
میخواهید و دقیقاً همان چیزی است که نباید به کسی بیرون از پلتفرم بدهید: او کدش را بر پایهٔ یک
شکل نوشته و قرار نیست چون شما par_total را به par_amount تغییر نام دادهاید، آن را بازنویسی کند.
اپیآی سفارشی این تعهد است که شکل یک مسیر ثابت بماند، حتی وقتی جدولهای پشت آن تغییر میکنند.
این یک مؤلفهٔ راهحل شماست، مانند یک فرم یا یک نما. در محیط توسعه ساخته میشود، در بستهٔ راهحل سفر میکند و در محیط آزمون و تولید پاسخ میدهد، چون شما آن را همانجا وارد کردهاید.
پیش از شروع
- یک راهحل، با یک ناشر (پیشوند ناشر بخشی از هر مسیری میشود که منتشر میکنید).
- جدولی که میخواهید در معرض قرار دهید، اگر این اپیآی دربارهٔ یک جدول است.
- یک ثبت برنامه برای سامانهای که شما را فرا میخواند، با همان دامنهای که میخواهید الزامی کنید. ثبت آن کار مدیر است — ثبت یک برنامهٔ بیرونی.
گامها
- راهحل خود را باز کنید و اپیآیهای سفارشی را انتخاب کنید.
- جدید را بزنید.
- یک نام نمایشی بدهید. نام منطقی خودش پر میشود، با پیشوند ناشر شما.
- نسخه را روی ۱ بگذارید. بعداً آن را بالا میبرید و نسخهٔ قبلی همچنان پاسخ میدهد تا فراخوان شما فرصت هماهنگی داشته باشد.
- روش را انتخاب و مسیر را بنویسید — بدون اسلش ابتدایی. بخش داخل آکولاد یک پارامتر مسیر
است:
invoices/{id}/approve. صفحه همزمان با تایپ، نشانی کامل را نشان میدهد. - دامنهٔ لازم را برگزینید — دامنهای که ثبت برنامهٔ سامانهٔ فراخوان باید داشته باشد، مانند
modelapps.read. این یک فهرست است نه جعبهٔ متن: دامنهای که به کسی داده نمیشود هر فراخوانی را رد میکند و شما تا وقتی یک شریک به آن برنخورد از آن باخبر نمیشوید. - برگزینید که با چه چی زی پاسخ میدهد — JSON، متن یا HTML. بخش پایین را ببینید؛ همین انتخاب اسکریپت آغازین را هم تعیین میکند، پس پیش از نوشتن کد آن را مشخص کنید.
- طرح درخواست و طرح پاسخ اختیاریاند. اگر خالی بمانند، قرارداد از اسکریپت شما خوانده
میشود — هر
getInputو هرsetOutput— و در سند OpenAPI منتشر میشود. JSON Schema را فقط برای سختگیری بیشتر بنویسید (نوعها، فیلدهای الزامی، محدودهها)؛ طرح درخواستِ نوشتهشده در هر فراخوانی بررسی میشود، پس درخواست نادرست همان دمِ در رد میشود. - تیک پاسخگو را نگه دارید و ذخیره کنید.
- سطر را برگزینید و ویرایش کد را بزنید. فهرست جای خود را به ویرایشگر میدهد — رفتار این اپیآی همینجا و به بیاس لنگ نوشته میشود. چشم را بزنید تا هر تابعی را که میتوانید فراخوانی کنید ببینید. ذخیره و سپس بستن.
با چه چیزی پاسخ میدهد
یک اپیآی سفارشی یکی از سه چیز را پاسخ میدهد و شما روی قرارداد انتخاب میکنید کدام:
| پاسخ با | اسکریپت مینویسد | مناسبِ |
|---|---|---|
| JSON | setOutput("name", value) برای هر فیلد | سامانهٔ دیگری که دادههای شما را میخواند |
| متن | setBody("...") | یک خط — یک شناسه، یک توکن، یک سطر CSV |
| HTML | setBody("<p>...</p>") | ایمیل تأیید، صفحهای که مرورگر باز میکند |
setOutput("reference", "BOOKING-42"); // JSON: { "reference": "BOOKING-42" }
setBody("BOOKING-42"); // متن یا HTML: دقیقاً همین و نه چیز دیگر
یکی را به کار ببرید، هرگز هر دو را. اسکریپتی که هم setOutput و هم setBody را صدا بزند
هنگام اجرا رد میشود، بهجای اینکه یکی بیصدا برنده شود — پاسخی که نمیتوانید توضیحش دهید بدتر از
خطایی است که میتوانید بخوانید.
دو تابع دیگر هم در کنارشان هست:
setStatus(404)— وضعیتی که میخواهید با آن پاسخ دهید. باthrowValidationErrorفرق دارد که یعنی فراخوان اشتباه کرده (خطای ۴۰۰)؛setStatusتصمیم خودِ قرارداد شماست. یک204نباید بدنه داشته باشد.setContentType("html")— نوع را برای یک فراخوانی تغییر میدهد. بیشتر اپیآیها هرگز به آن نیاز ندارند، چون قرارداد از پیش میگوید با چه چیزی پاسخ میدهند.
چرا قرارداد آن را اعلام میکند و نه فقط کد شما: سند OpenAPI پیش از آنکه کسی شما را فراخوانی کند ساخته میشود، پس تنها میتواند آنچه را قرارداد میگوید توصیف کند. HTML را اعلام کنید تا سند HTML بگوید؛ آن را روی JSON بگذارید و یک یکپارچهساز دور صفحهٔ شما تجزیهگر JSON مینویسد.
گذاشتن یک مقدار ذخیرهشده در صفحه
یک صفحه از متن ساخته میشود و بخشی از آن متن، رکوردهای شماست. هر مقدار ذخیرهشده را پیش از
گذاشتن در صفحه از htmlEncode بگذرانید:
var booking = getRecord('par_booking', getInput('id'));
setBody('<h1>Receipt ' + htmlEncode(booking.par_name) + '</h1>'
+ '<p>' + htmlEncode(booking.par_guest) + '</p>');
آنگاه مهمانی که در نام خود <b> یا بدتر از آن <script> تایپ کرده، همان متن نمایش داده میشود
و بخشی از رسیدی نمیشود که شخص دیگری باز میکند. فیلدی که کسی پر نکرده خالی برمیگردد، پس بهجای
واژهٔ null جای خالی میماند. همین برای ایمیلی که با html: true میفرستید هم صدق میکند. نشانهگذاری
خود شما، یعنی تگهایی که در اسکریپت نوشتهاید، کدگذاری نمیشود.
پاسخ دادن با فهرستی از رکوردها
setOutput یک رکورد و یک صفحه از رکوردها را به تمامی حمل میکند:
var rows = runQuery({
entity: 'par_invoice',
select: ['par_name', 'par_total'],
filter: { connector: 'And', conditions: [{ field: 'par_status', operator: 'Equals', value: 'Open' }] }
});
setOutput("count", rows.length());
setOutput("invoices", rows); // یک آرایهٔ JSON، برای هر سطر یک شیء
فراخوان { "count": 2, "invoices": [ { "id": "...", "par_name": "INV-1", ... } ] } را دریافت
میکند، با نام فیلدهای شما دقیقاً همانگونه که روی جدول هستند — و همین را در شمای پاسخ مینویسید
تا سند OpenAPI آن را توصیف کند.
نمیتوانید فهرستی از خودتان بسازید. بیاسلنگ راهی برای افزودن به یک فهرست ندارد، پس سطرهایی
که با آنها پاسخ میدهید همان سطرهاییاند که یک پرسوجو به شما داده است. بهجای تلاش برای سرِ هم
کردن ستونها از select استفاده کنید: ستونی که انتخاب نکردهاید ستونی است که سفر نمیکند و
همین است که یادداشت داخلی را از پاسخی که منتشر میکنید بیرون نگه میدارد.
پاسخ دادن به مشتریای که وارد شده است
هرچه تا اینجا گفته شد فرض میکند فراخوان یک سامانه است — یک ثبت برنامه که هرچه نقش امنیتیاش اجازه میدهد انجام میدهد. پورتال فرق دارد: parsware.com، یا هر سایتی که روی پورتالها میسازید، از طرف کسی که آنجا وارد شده شما را فراخوانی میکند، و تنها چیزی که عملیات شما باید درست انجام دهد این است که دربارهٔ شرکتِ او پاسخ دهد و نه هیچکس دیگر.
دقیقاً یک راه برای پرسیدن اینکه او کیست وجود دارد:
| تابع | پاسخ |
|---|---|
portalCustomerId() | مشتری (یک رکورد Account) که فرد واردشده به آن تعلق دارد |
portalUserId() | خودِ کاربر پورتال |
portalContactId() | رکورد Contact که او هست، یا تهی اگر به هیچکدام پیوند نخورده باشد |
portalEmail() | نشانیای که با آن وارد شده است |
portalRole() | «Owner»، «Billing» یا «Member» — آنچه از سوی شرکتش میتواند انجام دهد |
var customerId = portalCustomerId();
var rows = runQuery({
entity: 'par_invoice',
select: ['par_name', 'par_total'],
filter: { connector: 'And', conditions: [{ field: 'par_customer', operator: 'Equals', value: customerId }] }
});
setOutput("invoices", rows);
هرگز مشتری را بهعنوان ورودی نگیرید. اینکه در آن اسکریپت getInput("customerId") وجود
ندارد از قلم نیفتاده است — شناسهٔ مشتریای که فراخوان میتواند بفرستد شناسهای است که فراخوان
میتواند عوضش کند، و یک درخواست دستکاریشده فاکتورهای کسِ دیگری را میخواند. این مقدار از یک توکن
امضاشده میآید که هیچکس میان بازدیدکننده و شما نمیتواند بازنویسیاش کند.
دو چیز از این برمیآید و هر دو عمدیاند:
-
فراخوانی بدون فرد واردشده رد میشود، نه اینکه با چیزی تهی پاسخ بگیرد. اگر اسکریپت شما
portalCustomerId()را صدا بزند و درخواست هیچ ورودِ پورتالی نداشته باشد، فراخوان خطای ۴۰۱ میگیرد — بهجای اینکه اسکریپت شما مقداری تهی بگیرد و شاید بر اساس آن پالایش کند. -
portalRole()راهی است که چیزی را برای کسانی نگه دارید که باید ببینندش. مبلغ یک فاکتور یا روش پرداخت برای Owner یا کسی است که پول میدهد، نه برای هر کسی که حسابی دارد:if (portalRole() == "Member") {throwValidationError("این را از یک Owner یا مسئول مالی بخواهید.");} else {setOutput("total", rows[0].par_total);}این سه نقش یک نردباناند — Owner هرچه Billing میتواند انجام میدهد و Billing هرچه Member — پس آزمودنِ همان نقشی که نمیتواند، معمولاً کوتاهتر از شمردن نقشهایی است که میتوانند.
-
آنچه بازدیدکننده میتواند انجام دهد از آنچه سایت میتواند باریکتر است. عملیات مجاز اشتراکِ میان دسترسیهای برنامهٔ پورتال و دسترسیهای فرد واردشده است. سایتی با دسترسی گسترده، بازدیدکنندگانش را پرقدرت نمیکند.
رد کردن، اسکریپت شما را متوقف میکند
throwValidationError اسکریپت را در همان خطی که فراخوانی میشود تمام میکند. هیچچیز بعد از
آن اجرا نمیشود — نه بقیهٔ بلاک، نه خطهای پایینتر، و نه catchی که دور آن گذاشتهاید. فراخوان
خطای ۴۰۰ با جملهٔ شما میگیرد، و هرچه اسکریپت قرار بود بعد از آن بنویسد نوشته نمیشود.
پس اول بررسی کنید، و وقتی بررسیها پشت سر گذاشته شد بنویسید:
if (isEmpty(getInput("reference"))) {
throwValidationError("یک شناسه لازم است.");
}
createRecord('par_invoice', { par_name: getInput("reference") });
setStatus(201);
تنها چیزی که یک رد کردن نمیتواند برگرداند، نوشتنی است که پیش از آن انجام شده: اسکریپتی که رکوردی میسازد و بعد رد میکند، رکورد را ساخته است. رد کردنها را بالای نوشتنها بگذارید.
ثبت زمان
now() لحظهٔ کنونی و today() روز کنونی را پاسخ میدهند، هر دو بهصورت متنی که میتوانید
یکراست در یک فیلد تاریخ/زمان بنویسید:
createRecord('par_lead', { par_name: getInput("name"), par_received_at: now() });
اینها را بهجای زمانی که فراخوان برایتان فرستاده به کار ببرید. «این کِی رسید» واقعیتی دربارهٔ سکوی شماست؛ مهر زمانی در بدنهٔ درخواست ادعایی است که فراخوان کرده و میتواند هر تاریخی باشد. هر دو به وقت جهانی (UTC) پاسخ میدهند — تقویم خودِ خواننده هنگام نمایش اعمال میشود، نه هنگام ذخیره.
بررسی درستی کار
- سطر تازه نشانی ارائهشده را نشان میدهد —
/api/par/v1/invoicesیا هر چه ساختهاید. - در نوار ابزار OpenAPI را بزنید. سند تولیدشده در حافظه رونوشت میشود: آن را برای کسی که پرسیده نقطهٔ پایانی شما چه شکلی است بفرستید. این سند از تعریفهای شما مشتق میشود، پس نمیتواند چیزی جز آنچه سرور واقعاً انجام میدهد را توصیف کند.
- از سامانهٔ فراخوان بخواهید با نشانهٔ ثبت برنامهٔ خودش آن را امتحان کند.
نسخهبندی: بخشی که ارزش فهمیدن دارد
هویت یک اپیآی، نام و نسخهٔ آن با هم است. par_approve_invoice نسخهٔ ۱ و نسخهٔ ۲ یک قرارداد
در دو سن هستند و کنار هم پاسخ میدهند.
پس هرگاه لازم شد شکل درخواست یا پاسخ را طوری تغییر دهید که فراخوان متوجه شود:
- یک اپیآی سفارشی تازه با همان نام نمایشی و نسخهٔ ۲ بسازید. این تکراری نیست — صفحه آن را میپذیرد.
- به شریکهای یکپارچهسازی خبر دهید و به آنها فرصت بدهید.
- وقتی دیگر کسی نسخهٔ ۱ را فرا نمیخواند، تیک پاسخگو را از آن بردارید.
همین گام آخر است که معمولاً اشتباه انجام میشود. تیک پاسخگو را بردارید؛ حذف نکنید. نسخهٔ بازنشسته از پاسخدهی میافتد اما همچنان وجود دارد، در راهحل سفر میکند و همچنان مستند میکند که نسخهٔ ۱ چه تعهدی داشت. حذف، مسیر را بدون هیچ هشداری برمیدارد — هر فراخوانی خطای ۴۰۴ میگیرد که از «سرویس شما خراب است» قابل تشخیص نیست.
آنچه اپیآی سفارشی انجام نمیدهد
هرگز دسترسی را گسترده نمیکند. عملیات جدول از همان دری میگذرد که صفحههای خود پلتفرم از آن میگذرند: مجوز فراخوان روی آن جدول و سیاست دادهٔ پشت آن تعیین میکنند چه چیزی برمیگردد. قرارداد سفارشی به سامانهٔ بیرونی نمایی پایدار از دادههای شما میدهد، نه نمایی بزرگتر.
اگر سامانهٔ فراخوان کمتر از انتظار شما دریافت میکند، پاسخ تقریباً همیشه نقش امنیتی پشت ثبت برنامهٔ اوست، نه اپیآی.
عیبیابی
| آنچه میبینید | معنایش |
|---|---|
| هنگام ذخیره: «... از پیش پاسخ داده میشود» | اپیآی دیگری در این محیط همان روش و مسیر را در آن نسخه ادعا کرده است. دو قرارداد نمیتوانند یک نشانی را با هم داشته باشند. |
| «... از پیش نسخهٔ ۱ دارد» | شما بهجای نسخهبندی، دارید قرارداد را دوباره میسازید. نسخه را بالا ببرید. |
«... باید بخش {id} داشته باشد» | خواندن، بهروزرسانی و حذف روی یک رکورد کار میکنند و مسیر تنها جایی است که شناسهٔ آن رکورد میتواند از آن بیاید. |
| «درخواست GET بدنه ندارد» | شمای درخواست را بردارید یا روش را عوض کنید. |
| خطای ۴۰۳ نزد فراخوان | ثبت برنامهٔ او دامنهای را که شما الزامی کردهاید ندارد. |
| «هم setOutput و هم setBody فراخوانی شد» | اسکریپت شما پاسخ را دو بار میسازد. یکی را برگزینید: setOutput یک شیء JSON میسازد و setBody یک بدنهٔ متنی مینویسد. |
| «بدنه به همراه وضعیت ۲۰۴» | وضعیت ۲۰۴ به فراخوان میگوید چیزی برای خواندن نیست، پس هرگز آنچه نوشتهاید را نمیبیند. از ۲۰۰ استفاده کنید یا بدنه را بردارید. |
| «وضعیت HTTP میان ۱۰۰ و ۵۹۹ است» | به setStatus چیزی داده شده که وضعیت نیست. |
| صفحه با گیومه بستهبندی میرسد | اسکریپت بهجای setBody از setOutput استفاده کرده است. setOutput هر چه قرارداد اعلام کرده باشد، همیشه JSON پاسخ میدهد. |
| خطای ۴۰۴ نزد فراخوان | هیچ اپیآی پاسخگویی آن روش و مسیر را در آن نسخه پاسخ نمیدهد. «پاسخگو» و بخش نسخه را بررسی کنید. |
| خطای ۴۰۱ با «به کاربر واردشدهٔ پورتال نیاز دارد» | اسکریپت شما portalCustomerId() را صدا میزند و درخواست هیچ ورودِ پورتالی نداشته است. یا فراخوان یک سامانه است و نه پورتال، یا نشست بازدیدکننده منقضی شده است. |
| خطای ۴۰۱ با «معتبر نیست» | ورودِ پورتالی ارائه شده و پذیرفته نشده است — منقضی، یا صادرشده برای پورتالی دیگر. |
| خطای ۴۰۳ با «هنوز به هیچ شرکتی تعلق ندارد» | فرد وارد شده اما در هیچ شرکتی قرار نگرفته است. بهجای فهرستی خالی همین را به او نشان دهید. |
| با اینکه درخواست را رد کردید رکورد ساخته شد | نوشتن پیش از رد کردن اجرا شده است. throwValidationError اسکریپت را همانجا که فراخوانی میشود متوقف میکند؛ بررسی را بالای نوشتن ببرید. |