ActiveResource.js: إنشاء مجموعة تطوير برامج JavaScript SDK قوية لواجهة برمجة تطبيقات JSON الخاصة بك ، بسرعة

نشرت: 2022-03-11

أطلقت شركتك للتو واجهة برمجة التطبيقات الخاصة بها وتريد الآن بناء مجتمع من المستخدمين حولها. أنت تعلم أن معظم عملائك سيعملون في JavaScript ، لأن الخدمات التي توفرها واجهة برمجة التطبيقات الخاصة بك تسهل على العملاء إنشاء تطبيقات ويب بدلاً من كتابة كل شيء بأنفسهم - Twilio هو مثال جيد على ذلك.

أنت تعلم أيضًا أنه بقدر ما قد تكون واجهة برمجة تطبيقات RESTful الخاصة بك ، سيرغب المستخدمون في إسقاط حزمة JavaScript التي ستقوم بكل الأعمال الثقيلة نيابة عنهم. لن يرغبوا في معرفة واجهة برمجة التطبيقات الخاصة بك وبناء كل طلب يحتاجون إليه بأنفسهم.

إذن أنت تبني مكتبة حول واجهة برمجة التطبيقات الخاصة بك. أو ربما تكتب فقط نظام إدارة حالة لتطبيق ويب يتفاعل مع واجهة برمجة التطبيقات الداخلية الخاصة بك.

في كلتا الحالتين ، لا تريد أن تكرر نفسك مرارًا وتكرارًا في كل مرة تقوم فيها بحشد أحد موارد API الخاصة بك ، أو الأسوأ من ذلك ، أن CRUD مورد متعلق بهذه الموارد. هذا ليس جيدًا لإدارة SDK المتنامي على المدى الطويل ، كما أنه ليس استخدامًا جيدًا لوقتك.

بدلاً من ذلك ، يمكنك استخدام ActiveResource.js ، وهو نظام JavaScript ORM للتفاعل مع واجهات برمجة التطبيقات. لقد قمت بإنشائه لسد حاجة كانت لدينا في مشروع: لإنشاء JavaScript SDK في أقل عدد ممكن من الأسطر. وقد أتاح ذلك أقصى قدر من الكفاءة لنا ولمجتمع المطورين لدينا.

وهو يقوم على المبادئ الكامنة وراء آلية ActiveRecord ORM البسيطة الخاصة بـ Ruby on Rails.

مبادئ JavaScript SDK

هناك فكرتان من أفكار Ruby on Rails التي وجهت تصميم ActiveResource.js:

  1. "الاتفاقية على التكوين": ضع بعض الافتراضات حول طبيعة نقاط نهاية API. على سبيل المثال ، إذا كان لديك مورد Product ، فإنه يتوافق مع نقطة نهاية /products . بهذه الطريقة لا يتم قضاء الوقت بشكل متكرر في تكوين كل طلبات SDK لواجهة برمجة التطبيقات الخاصة بك. يمكن للمطورين إضافة موارد API جديدة مع استعلامات CRUD المعقدة إلى SDK المتزايد الخاص بك في دقائق ، وليس ساعات.
  2. "Exalt beautiful code:" منشئ Rails ، DHH قال ذلك بشكل أفضل - هناك شيء رائع حول الكود الجميل في حد ذاته. يقوم ActiveResource.js أحيانًا بتغليف الطلبات القبيحة بمظهر خارجي جميل. لم تعد مضطرًا إلى كتابة تعليمات برمجية مخصصة لإضافة عوامل التصفية وتقسيم الصفحات وتضمين العلاقات المتداخلة في العلاقات مع طلبات GET. ولا يتعين عليك إنشاء طلبات POST و PATCH التي تأخذ التغييرات في خصائص الكائن وإرسالها إلى الخادم للتحديث. بدلاً من ذلك ، ما عليك سوى استدعاء طريقة على ActiveResource: لا مزيد من اللعب مع JSON للحصول على الطلب الذي تريده ، فقط عليك القيام بذلك مرة أخرى للطلب التالي.

قبل أن نبدأ

من المهم ملاحظة أنه في وقت كتابة هذا التقرير ، كان ActiveResource.js يعمل فقط مع واجهات برمجة التطبيقات المكتوبة وفقًا لمعيار JSON: API.

إذا لم تكن معتادًا على JSON: API وتريد المتابعة ، فهناك العديد من المكتبات الجيدة لإنشاء خادم JSON: API.

ومع ذلك ، فإن ActiveResource.js هو DSL أكثر من كونه غلافًا لمعيار API معين. يمكن توسيع الواجهة التي تستخدمها للتفاعل مع واجهة برمجة التطبيقات الخاصة بك ، لذلك يمكن أن تغطي المقالات المستقبلية كيفية استخدام ActiveResource.js مع واجهة برمجة التطبيقات المخصصة الخاصة بك.

إعداد الأمور

للبدء ، قم بتثبيت active-resource في مشروعك:

 yarn add active-resource

الخطوة الأولى هي إنشاء ResourceLibrary لواجهة برمجة التطبيقات الخاصة بك. سأضع كل ActiveResource الخاصة بي في مجلد src/resources :

 // /src/resources/library.js import { createResourceLibrary } from 'active-resource'; const library = createResourceLibrary('http://example.com/api/v1'); export default library;

المعلمة الوحيدة المطلوبة createResourceLibrary هي عنوان URL الجذر لواجهة برمجة التطبيقات الخاصة بك.

ما سنقوم بإنشائه

سنقوم بإنشاء مكتبة JavaScript SDK لواجهة برمجة تطبيقات نظام إدارة المحتوى. هذا يعني أنه سيكون هناك مستخدمون ومنشورات وتعليقات وإخطارات.

سيتمكن المستخدمون من قراءة المنشورات وإنشائها وتعديلها ؛ قراءة وإضافة وحذف التعليقات (على المنشورات أو على تعليقات أخرى) ، وتلقي الإخطارات بالمشاركات والتعليقات الجديدة.

لن أستخدم أي مكتبة محددة لإدارة العرض (React ، Angular ، إلخ) أو الحالة (Redux ، إلخ) ، بدلاً من ذلك ، استخرج البرنامج التعليمي للتفاعل فقط مع API الخاص بك من خلال ActiveResource s.

المصدر الأول: المستخدمون

سنبدأ بإنشاء مورد User لإدارة مستخدمي نظام إدارة المحتوى.

أولاً ، نقوم بإنشاء فئة موارد User ببعض attributes :

 // /src/resources/User.js import library from './library'; class User extends library.Base { static define() { this.attributes('email', 'userName', 'admin'); } } export default library.createResource(User);

لنفترض الآن أن لديك نقطة نهاية للمصادقة ، وبمجرد أن يرسل المستخدم بريده الإلكتروني وكلمة المرور ، يقوم بإرجاع رمز وصول ومعرف المستخدم. تتم إدارة نقطة النهاية هذه بواسطة بعض وظائف requestToken . بمجرد حصولك على معرف المستخدم المصادق عليه ، فأنت تريد تحميل جميع بيانات المستخدم:

 import library from '/src/resources/library'; import User from '/src/resources/User'; async function authenticate(email, password) { let [accessToken, userId] = requestToken(email, password); library.headers = { Authorization: 'Bearer ' + accessToken }; return await User.find(userId); }

لقد قمت بتعيين library.headers على أن يكون لها رأس Authorization مع accessToken بحيث يتم تفويض جميع الطلبات المستقبلية من ResourceLibrary .

سيغطي قسم لاحق كيفية مصادقة مستخدم وتعيين رمز الوصول باستخدام فئة موارد User فقط.

الخطوة الأخيرة authenticate هي طلب User.find(id) . سيؤدي هذا إلى تقديم طلب إلى /api/v1/users/:id ، وقد تبدو الاستجابة كما يلي:

 { "data": { "type": "users", "id": "1", "attributes": { "email": "[email protected]", "user_name": "user1", "admin": false } } }

ستكون الاستجابة من authenticate مثيلاً لفئة User . من هنا ، يمكنك الوصول إلى السمات المختلفة للمستخدم المصادق عليه ، إذا كنت تريد عرضها في مكان ما في التطبيق.

 let user = authenticate(email, password); console.log(user.id) // '1' console.log(user.userName) // user1 console.log(user.email) // [email protected] console.log(user.attributes()) /* { email: '[email protected]', userName: 'user1', admin: false } */

سيصبح كل اسم من أسماء السمات باسم camelCased ، لتلائم المعايير النموذجية لجافا سكريبت. يمكنك الحصول على كل منها مباشرة كخصائص لكائن user ، أو الحصول على كل السمات عن طريق استدعاء user.attributes() .

إضافة فهرس الموارد

قبل أن نضيف المزيد من الموارد التي تتعلق بفئة User ، مثل الإشعارات ، يجب أن نضيف ملفًا ، src/resources/index.js ، سيفهرس جميع مواردنا. هذا له فائدتان:

  1. سيؤدي ذلك إلى تنظيف وارداتنا من خلال السماح لنا بتدمير src/resources متعددة في بيان استيراد واحد بدلاً من استخدام عبارات استيراد متعددة.
  2. سيقوم بتهيئة جميع الموارد على ResourceLibrary التي سننشئها عن طريق استدعاء library.createResource على كل منها ، وهو أمر ضروري لـ ActiveResource.js لبناء العلاقات.
 // /src/resources/index.js import User from './User'; export { User };

إضافة مورد ذي صلة

لنقم الآن بإنشاء مورد ذي صلة User ، وهو Notification . قم أولاً بإنشاء فئة Notification belongsTo إلى فئة User :

 // /src/resources/Notification.js import library from './library'; class Notification extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Notification);

ثم نضيفه إلى فهرس الموارد:

 // /src/resources/index.js import Notification from './Notification'; import User from './User'; export { Notification, User };

بعد ذلك ، اربط الإخطارات بفئة User :

 // /src/resources/User.js class User extends library.Base { static define() { /* ... */ this.hasMany('notifications'); } }

الآن ، بمجرد استعادة المستخدم من authenticate ، يمكننا تحميل وعرض جميع إشعاراته:

 let notifications = await user.notifications().load(); console.log(notifications.map(notification => notification.message));

يمكننا أيضًا تضمين الإشعارات في طلبنا الأصلي للمستخدم المصادق عليه:

 async function authenticate(email, password) { /* ... */ return await User.includes('notifications').find(userId); }

هذا هو أحد الخيارات العديدة المتوفرة في DSL.

مراجعة DSL

دعنا نغطي ما هو ممكن بالفعل لطلبه فقط من الكود الذي كتبناه حتى الآن.

يمكنك الاستعلام عن مجموعة من المستخدمين ، أو مستخدم واحد.

 let users = await User.all(); let user = await User.first(); user = await User.last(); user = await User.find('1'); user = await User.findBy({ userName: 'user1' });

يمكنك تعديل الاستعلام باستخدام طرق علائقية قابلة للتسلسل:

 // Query and iterate over all users User.each((user) => console.log(user)); // Include related resources let users = await User.includes('notifications').all(); // Only respond with user emails as the attributes users = await User.select('email').all(); // Order users by attribute users = await User.order({ email: 'desc' }).all(); // Paginate users let usersPage = await User.page(2).perPage(5).all(); // Filter users by attribute users = await User.where({ admin: true }).all(); users = await User .includes('notifications') .select('email', { notifications: ['message', 'createdAt'] }) .order({ email: 'desc' }) .where({ admin: false }) .perPage(10) .page(3) .all(); let user = await User .includes('notification') .select('email') .first();

لاحظ أنه يمكنك إنشاء الاستعلام باستخدام أي قدر من المُعدِّلات المتسلسلة ، وأنه يمكنك إنهاء الاستعلام باستخدام .first() .all() .first () أو .last() أو .each ( .each() .

يمكنك بناء مستخدم محليًا ، أو إنشاء مستخدم على الخادم:

 let user = User.build(attributes); user = await User.create(attributes);

بمجرد أن يكون لديك مستخدم مستمر ، يمكنك إرسال التغييرات إليه ليتم حفظها على الخادم:

 user.email = '[email protected]'; await user.save(); /* or */ await user.update({ email: '[email protected]' });

يمكنك أيضًا حذفه من الخادم:

 await user.destroy();

يمتد خط DSL الأساسي هذا إلى الموارد ذات الصلة أيضًا ، كما سأوضح في بقية البرنامج التعليمي. يمكننا الآن تطبيق ActiveResource.js بسرعة لإنشاء بقية CMS: المنشورات والتعليقات.

إنشاء المشاركات

أنشئ فئة موارد للنشر Post بفئة User :

 // /src/resources/Post.js import library from './library'; class Post extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Post);
 // /src/resources/User.js class User extends library.Base { static define() { /* ... */ this.hasMany('notifications'); this.hasMany('posts'); } }

أضف Post إلى فهرس الموارد أيضًا:

 // /src/resources/index.js import Notification from './Notification'; import Post from './Post'; import User from './User'; export { Notification, Post, User };

ثم اربط مورد Post في نموذج للمستخدمين لإنشاء منشورات وتعديلها. عندما يزور المستخدم النموذج لإنشاء منشور جديد لأول مرة ، سيتم إنشاء مورد Post ، وفي كل مرة يتم تغيير النموذج ، نطبق التغيير على Post :

 import Post from '/src/resources/Post'; let post = Post.build({ user: authenticatedUser }); onChange = (event) => { post.content = event.target.value; };

بعد ذلك ، أضف رد اتصال onSubmit إلى النموذج لحفظ المنشور على الخادم ، ومعالجة الأخطاء إذا فشلت محاولة الحفظ:

 onSubmit = async () => { try { await post.save(); /* successful, redirect to edit post form */ } catch { post.errors().each((field, error) => { console.log(field, error.message) }); } }

تحرير المشاركات

بمجرد حفظ المنشور ، سيتم ربطه بواجهة برمجة التطبيقات الخاصة بك كمورد على الخادم الخاص بك. يمكنك معرفة ما إذا كان المورد مستمرًا على الخادم عن طريق الاتصال persisted :

 if (post.persisted()) { /* post is on server */ }

بالنسبة إلى الموارد المستمرة ، يدعم ActiveResource.js السمات القذرة ، حيث يمكنك التحقق مما إذا تم تغيير أي سمة لمورد من قيمته على الخادم.

إذا قمت باستدعاء save() على مورد مستمر ، فسيتم تقديم طلب PATCH يحتوي فقط على التغييرات التي تم إجراؤها على المورد ، بدلاً من إرسال مجموعة السمات والعلاقات الكاملة للمورد إلى الخادم دون داع.

يمكنك إضافة السمات المتعقبة إلى مورد باستخدام إعلان attributes . دعنا نتتبع التغييرات إلى post.content :

 // /src/resources/Post.js class Post extends library.Base { static define() { this.attributes('content'); /* ... */ } }

الآن ، مع منشور مستمر على الخادم ، يمكننا تعديل المنشور ، وعندما يتم النقر فوق زر الإرسال ، احفظ التغييرات على الخادم. يمكننا أيضًا تعطيل زر الإرسال إذا لم يتم إجراء أي تغييرات بعد:

 onEdit = (event) => { post.content = event.target.value; } onSubmit = async () => { try { await post.save(); } catch { /* display edit errors */ } } disableSubmitButton = () => { return !post.changed(); }

هناك طرق لإدارة علاقة فردية مثل post.user() ، إذا أردنا تغيير المستخدم المرتبط بمنشور:

 await post.updateUser(user);

هذا يعادل:

 await post.update({ user });

مصدر التعليق

الآن قم بإنشاء Comment لفئة الموارد وربطها Post . تذكر مطلبنا بأن التعليقات يمكن أن تكون ردًا على منشور أو تعليق آخر ، لذا فإن المورد ذي الصلة للتعليق متعدد الأشكال:

 // /src/resources/Comment.js import library from './library'; class Comment extends library.Base { static define() { this.attributes('content'); this.belongsTo('resource', { polymorphic: true, inverseOf: 'replies' }); this.belongsTo('user'); this.hasMany('replies', { as: 'resource', className: 'Comment', inverseOf: 'resource' }); } } export default library.createResource(Comment);

تأكد من إضافة Comment إلى /src/resources/index.js أيضًا.

سنحتاج إلى إضافة سطر إلى فئة Post أيضًا:

 // /src/resources/Post.js class Post extends library.Base { static define() { /* ... */ this.hasMany('replies', { as: 'resource', className: 'Comment', inverseOf: 'resource' }); } }

يشير الخيار inverseOf الذي تم تمريره إلى hasMany تعريف replies إلى أن هذه العلاقة هي معكوس متعدد الأشكال belongsTo إلى تعريف resource . تُستخدم خاصية inverseOf للعلاقات بشكل متكرر عند إجراء عمليات بين العلاقات. عادةً ، سيتم تحديد هذه الخاصية تلقائيًا عبر اسم الفئة ، ولكن نظرًا لأن العلاقات متعددة الأشكال يمكن أن تكون واحدة من فئات متعددة ، يجب عليك تحديد الخيار inverseOf بنفسك حتى تتمتع العلاقات متعددة الأشكال بنفس الوظائف مثل العلاقات العادية.

إدارة التعليقات على المشاركات

نفس خط المشترك الرقمي (DSL) الذي ينطبق على الموارد ينطبق أيضًا على إدارة الموارد ذات الصلة. الآن بعد أن أقمنا العلاقات بين المنشورات والتعليقات ، هناك عدد من الطرق التي يمكننا من خلالها إدارة هذه العلاقة.

يمكنك إضافة تعليق جديد إلى منشور:

 onSubmitComment = async (event) => { let comment = await post.replies().create({ content: event.target.value, user: user }); }

يمكنك إضافة رد على تعليق:

 onSubmitReply = async (event) => { let reply = await comment.replies().create({ content: event.target.value, user: user }); }

يمكنك تحرير تعليق:

 onEditComment = async (event) => { await comment.update({ content: event.target.value }); }

يمكنك إزالة تعليق من منشور:

 onDeleteComment = async (comment) => { await post.replies().delete(comment); }

عرض المشاركات والتعليقات

يمكن استخدام SDK لعرض قائمة مرقمة من المنشورات ، وعند النقر فوق منشور ، يتم تحميل المنشور على صفحة جديدة بكل تعليقاته:

 import { Post } from '/src/resources'; let postsPage = await Post .order({ createdAt: 'desc' }) .select('content') .perPage(10) .all();

سيقوم الاستعلام أعلاه باسترداد أحدث 10 منشورات ، ولتحسين السمة الوحيدة التي يتم تحميلها هي content .

إذا نقر المستخدم على زر للانتقال إلى الصفحة التالية من المنشورات ، فسيقوم معالج التغيير باسترداد الصفحة التالية. هنا نقوم أيضًا بتعطيل الزر إذا لم تكن هناك صفحات تالية.

 onClickNextPage = async () => { postsPage = await postsPage.nextPage(); if (!postsPage.hasNextPage()) { /* disable next page button */ } };

عند النقر على رابط منشور ، نفتح صفحة جديدة عن طريق تحميل وعرض المنشور بكل بياناته ، بما في ذلك التعليقات - المعروفة بالردود - وكذلك الردود على تلك الردود:

 import { Post } from '/src/resources'; onClick = async (postId) => { let post = await Post.includes({ replies: 'replies' }).find(postId); console.log(post.content, post.createdAt); post.replies().target().each(comment => { console.log( comment.content, comment.replies.target().map(reply => reply.content).toArray() ); }); }

سيؤدي استدعاء .target() على علاقة hasMany مثل ActiveResource.Collection post.replies() إلى إرجاع مجموعة التعليقات التي تم تحميلها وتخزينها محليًا.

هذا التمييز مهم ، لأن post.replies().target().first() سيعيد أول تعليق تم تحميله. في المقابل ، post.replies().first() سيعيد وعدًا لتعليق واحد مطلوب من GET /api/v1/posts/:id/replies .

يمكنك أيضًا طلب الردود على المنشور بشكل منفصل عن طلب المنشور نفسه ، مما يسمح لك بتعديل استفسارك. يمكنك إجراء سلسلة من المُعدِّلات مثل order ، select ، perPage ، لكل page ، where الاستعلام hasMany includes مثلما يمكنك عند الاستعلام عن الموارد نفسها.

 import { Post } from '/src/resources'; onClick = async (postId) => { let post = await Post.find(postId); let userComments = await post.replies().where({ user: user }).perPage(3).all(); console.log('Your comments:', userComments.map(comment => comment.content).toArray()); }

تعديل الموارد بعد طلبها

في بعض الأحيان تريد أخذ البيانات من الخادم وتعديلها قبل استخدامها. على سبيل المثال ، يمكنك التفاف post.createdAt في moment() بحيث يمكنك عرض تاريخ ووقت سهل الاستخدام للمستخدم حول وقت إنشاء المنشور:

 // /src/resources/Post.js import moment from 'moment'; class Post extends library.Base { static define() { /* ... */ this.afterRequest(function() { this.createdAt = moment(this.createdAt); }); } }

ثبات

إذا كنت تعمل مع نظام إدارة حالة يفضل الكائنات غير القابلة للتغيير ، فيمكن جعل كل السلوك في ActiveResource.js غير قابل للتغيير من خلال تكوين مكتبة الموارد:

 // /src/resources/library.js import { createResourceLibrary } from 'active-resource'; const library = createResourceLibrary( 'http://example.com/api/v1', { immutable: true } ); export default library;

إعادة الدوران: ربط نظام المصادقة

للختام ، سأوضح لك كيفية دمج نظام مصادقة المستخدم الخاص بك في User ActiveResource الخاص بك.

انقل نظام مصادقة الرمز المميز الخاص بك إلى نقطة نهاية API /api/v1/tokens . عندما يتم إرسال البريد الإلكتروني وكلمة المرور للمستخدم إلى نقطة النهاية هذه ، سيتم إرسال بيانات المستخدم المصادق عليها بالإضافة إلى رمز التفويض المميز استجابةً لذلك.

قم بإنشاء فئة مورد Token التي تنتمي إلى User :

 // /src/resources/Token.js import library from './library'; class Token extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Token);

أضف /src/resources/index.js Token

بعد ذلك ، أضف طريقة ثابتة authenticate إلى فئة موارد User الخاصة بك ، وربط User Token :

 // /src/resources/User.js import library from './library'; import Token from './Token'; class User { static define() { /* ... */ this.hasOne('token'); } static async authenticate(email, password) { let user = this.includes('token').build({ email, password }); let authUser = await this.interface().post(Token.links().related, user); let token = authUser.token(); library.headers = { Authorization: 'Bearer ' + token.id }; return authUser; } }

تستخدم هذه الطريقة ResourceLibrary.interface resourceLibrary.interface() ، والتي في هذه الحالة هي واجهة JSON: API ، لإرسال مستخدم إلى /api/v1/tokens . هذا صالح: نقطة النهاية في JSON: لا تتطلب واجهة برمجة التطبيقات (API) أن تكون الأنواع الوحيدة التي يتم إرسالها إليها ومنها هي تلك التي تم تسميتها باسمها. لذلك سيكون الطلب:

 { "data": { "type": "users", "attributes": { "email": "[email protected]", "password": "password" } } }

ستكون الاستجابة هي المستخدم المصادق عليه مع تضمين رمز المصادقة المميز:

 { "data": { "type": "users", "id": "1", "attributes": { "email": "[email protected]", "user_name": "user1", "admin": false }, "relationships": { "token": { "data": { "type": "tokens", "id": "Qcg6yI1a5qCxXgKWtSAbZ2MIHFChHAq0Vc1Lo4TX", } } } }, "included": [{ "type": "tokens", "id": "Qcg6yI1a5qCxXgKWtSAbZ2MIHFChHAq0Vc1Lo4TX", "attributes": { "expires_in": 3600 } }] }

ثم نستخدم token.id لتعيين رأس Authorization الخاص بمكتبتنا ، وإرجاع المستخدم ، وهو نفس طلب المستخدم عبر User.find() كما فعلنا من قبل.

الآن ، إذا اتصلت User.authenticate(email, password) ، فستتلقى مستخدمًا مصدقًا ردًا على ذلك ، وسيتم تفويض جميع الطلبات المستقبلية برمز وصول.

يتيح ActiveResource.js تطوير Rapid JavaScript SDK

في هذا البرنامج التعليمي ، اكتشفنا الطرق التي يمكن أن يساعدك بها ActiveResource.js في إنشاء JavaScript SDK بسرعة لإدارة موارد واجهة برمجة التطبيقات ومواردها المتنوعة ، والمعقدة أحيانًا ، ذات الصلة. يمكنك رؤية كل هذه الميزات وأكثر توثيقًا في README لـ ActiveResource.js.

أتمنى أن تكون قد استمتعت بالسهولة التي يمكن بها إجراء هذه العمليات ، وأنك ستستخدم (وربما تساهم أيضًا في) مكتبتي لمشاريعك المستقبلية إذا كانت تناسب احتياجاتك. بروح المصادر المفتوحة ، العلاقات العامة موضع ترحيب دائمًا!