@ecopex/ecopex-framework 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.
Files changed (81) hide show
  1. package/.env +73 -0
  2. package/README.md +248 -0
  3. package/bun.lockb +0 -0
  4. package/config/swagger/admin.js +44 -0
  5. package/config/swagger/api.js +19 -0
  6. package/database/migrations/20240000135243_timezones.js +22 -0
  7. package/database/migrations/20240000135244_countries.js +23 -0
  8. package/database/migrations/20240000135244_create_admins_table.js +66 -0
  9. package/database/migrations/20240000135244_currencies.js +21 -0
  10. package/database/migrations/20240000135244_languages.js +21 -0
  11. package/database/migrations/20240000135244_taxes.js +10 -0
  12. package/database/migrations/20240000135245_sites.js +37 -0
  13. package/database/migrations/20240000135246_payment_methods.js +33 -0
  14. package/database/migrations/20251016113547_devices.js +37 -0
  15. package/database/migrations/20251019192600_users.js +62 -0
  16. package/database/migrations/20251019213551_language_lines.js +35 -0
  17. package/database/migrations/20251222214131_category_groups.js +18 -0
  18. package/database/migrations/20251222214619_categories.js +27 -0
  19. package/database/migrations/20251222214848_brands.js +23 -0
  20. package/database/migrations/20251222214946_products.js +30 -0
  21. package/database/migrations/20251222215428_product_images.js +18 -0
  22. package/database/migrations/20251222215553_options.js +30 -0
  23. package/database/migrations/20251222215806_variants.js +23 -0
  24. package/database/migrations/20251222215940_attributes.js +25 -0
  25. package/database/migrations/20251222220135_discounts.js +15 -0
  26. package/database/migrations/20251222220253_reviews.js +22 -0
  27. package/database/migrations/20251222220341_favorites.js +10 -0
  28. package/database/migrations/20251222220422_search_logs.js +17 -0
  29. package/database/migrations/20251222220636_orders.js +16 -0
  30. package/database/migrations/20251222220806_order_items.js +19 -0
  31. package/database/migrations/20251222221317_order_statuses.js +10 -0
  32. package/database/migrations/20251222221446_order_payments.js +13 -0
  33. package/database/migrations/20251222221654_order_addresses.js +23 -0
  34. package/database/migrations/20251222221807_order_status_logs.js +13 -0
  35. package/database/seeds/admins.js +37 -0
  36. package/database/seeds/countries.js +203 -0
  37. package/database/seeds/currencies.js +165 -0
  38. package/database/seeds/languages.js +113 -0
  39. package/database/seeds/timezones.js +149 -0
  40. package/ecosystem.config.js +26 -0
  41. package/env.example +73 -0
  42. package/knexfile.js +3 -0
  43. package/libraries/2fa.js +22 -0
  44. package/libraries/aws.js +63 -0
  45. package/libraries/bcrypt.js +284 -0
  46. package/libraries/controls.js +113 -0
  47. package/libraries/date.js +14 -0
  48. package/libraries/general.js +8 -0
  49. package/libraries/image.js +57 -0
  50. package/libraries/jwt.js +178 -0
  51. package/libraries/knex.js +7 -0
  52. package/libraries/slug.js +14 -0
  53. package/libraries/stores.js +22 -0
  54. package/libraries/upload.js +194 -0
  55. package/locales/en/messages.json +4 -0
  56. package/locales/en/sql.json +3 -0
  57. package/locales/en/validation.json +52 -0
  58. package/locales/es/validation.json +52 -0
  59. package/locales/tr/validation.json +59 -0
  60. package/package.json +75 -0
  61. package/routes/admin/auto/admins.json +63 -0
  62. package/routes/admin/auto/devices.json +37 -0
  63. package/routes/admin/auto/migrations.json +21 -0
  64. package/routes/admin/auto/users.json +61 -0
  65. package/routes/admin/middlewares/index.js +87 -0
  66. package/routes/admin/spec/auth.js +626 -0
  67. package/routes/admin/spec/users.js +3 -0
  68. package/routes/auto/handler.js +635 -0
  69. package/routes/common/auto/countries.json +28 -0
  70. package/routes/common/auto/currencies.json +26 -0
  71. package/routes/common/auto/languages.json +26 -0
  72. package/routes/common/auto/taxes.json +46 -0
  73. package/routes/common/auto/timezones.json +29 -0
  74. package/stores/base.js +73 -0
  75. package/stores/index.js +195 -0
  76. package/utils/i18n.js +187 -0
  77. package/utils/jsonRouteLoader.js +587 -0
  78. package/utils/middleware.js +154 -0
  79. package/utils/routeLoader.js +227 -0
  80. package/workers/admin.js +124 -0
  81. package/workers/api.js +106 -0
@@ -0,0 +1,26 @@
1
+ {
2
+ "table": "languages",
3
+ "primary_key": "language_id",
4
+ "store": "languages",
5
+ "schema": {
6
+ "language_id": { "type": "integer", "example": 1 },
7
+ "code": { "type": "string", "default": "", "example": "en" },
8
+ "flag": { "type": "string", "default": "", "example": "🇬🇧" },
9
+ "name": { "type": "string", "default": "", "example": "English" }
10
+ },
11
+ "routes": [
12
+ {
13
+ "action": "list",
14
+ "method": "GET",
15
+ "path": "/languages",
16
+ "searchable_fields": ["code", "name", "flag"],
17
+ "pagination": true,
18
+ "additionalProperties": false
19
+ },
20
+ {
21
+ "action": "get",
22
+ "method": "GET",
23
+ "path": "/languages/:language_id"
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "table": "taxes",
3
+ "primary_key": "tax_id",
4
+ "schema": {
5
+ "tax_id": { "type": "integer", "example": 1 },
6
+ "name": { "type": "string", "search_type": "like", "creatable": true, "create_required": true, "updatable": true, "example": "VAT" },
7
+ "rate": { "type": "number", "minimum": 0, "maximum": 1, "creatable": true, "create_required": true, "updatable": true, "example": 0.18 }
8
+ },
9
+ "store": "taxes",
10
+ "routes": [
11
+ {
12
+ "action": "list",
13
+ "method": "GET",
14
+ "path": "/taxes",
15
+ "searchable_fields": ["name", "rate"],
16
+ "pagination": true,
17
+ "additionalProperties": false
18
+ },
19
+ {
20
+ "action": "get",
21
+ "method": "GET",
22
+ "path": "/taxes/:tax_id"
23
+ },
24
+ {
25
+ "action": "create",
26
+ "method": "POST",
27
+ "path": "/taxes",
28
+ "security": "auth",
29
+ "additionalProperties": false
30
+ },
31
+ {
32
+ "action": "update",
33
+ "method": "PUT",
34
+ "path": "/taxes/:tax_id",
35
+ "security": "auth",
36
+ "additionalProperties": false
37
+ },
38
+ {
39
+ "action": "delete",
40
+ "method": "DELETE",
41
+ "path": "/taxes/:tax_id",
42
+ "security": "auth",
43
+ "additionalProperties": false
44
+ }
45
+ ]
46
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "table": "timezones",
3
+ "primary_key": "timezone_id",
4
+ "store": "timezones",
5
+ "schema": {
6
+ "timezone_id": { "type": "integer", "search_type": "equal", "example": 1 },
7
+ "name": { "type": "string", "default": "", "search_type": "like", "example": "UTC" },
8
+ "code": { "type": "string", "default": "", "search_type": "like", "example": "UTC+00:00" },
9
+ "offset": { "type": "string", "default": "", "search_type": "like", "example": "+00:00" },
10
+ "gmt": { "type": "string", "default": "", "search_type": "like", "example": "UTC" }
11
+ },
12
+ "routes": [
13
+ {
14
+ "action": "list",
15
+ "method": "GET",
16
+ "path": "/timezones",
17
+ "searchable_fields": ["name", "code", "offset", "gmt"],
18
+ "pagination": true,
19
+ "additionalProperties": false
20
+ },
21
+ {
22
+ "action": "get",
23
+ "method": "GET",
24
+ "path": "/timezones/:timezone_id",
25
+ "security": "auth",
26
+ "additionalProperties": false
27
+ }
28
+ ]
29
+ }
package/stores/base.js ADDED
@@ -0,0 +1,73 @@
1
+ const knex = require('@root/libraries/knex');
2
+
3
+ module.exports = class StoreModel {
4
+ constructor(name = 'store_model', primary_key = 'id', schema = {}) {
5
+ this.name = name;
6
+ this.knex = knex;
7
+ this.data = new Map();
8
+ this.primary_key = primary_key;
9
+ this.schema = schema;
10
+ this.initialized = false;
11
+ }
12
+
13
+ async init() {
14
+ await this.get_data();
15
+ this.initialized = true;
16
+ }
17
+
18
+ async get_data() {
19
+ const data = await this.knex(this.name).select(Object.keys(this.schema));
20
+ data.forEach(item => {
21
+ this.data.set(item[this.primary_key], item);
22
+ });
23
+ return this.data;
24
+ }
25
+
26
+ async get_data_by_primary_key(primary_key) {
27
+ const data = this.data.get(primary_key);
28
+ if(!data) {
29
+ throw new Error('Data not found');
30
+ }
31
+ return data;
32
+ }
33
+
34
+ async create_data(data) {
35
+ const validated_data = this.validate_data(data);
36
+ const check_data = await this.knex(this.name).where(this.primary_key, validated_data[this.primary_key]).first();
37
+ if(check_data) {
38
+ throw new Error('Data already exists');
39
+ }
40
+ const [primary_key] = await this.knex(this.name).insert(validated_data);
41
+ const data_created = await this.knex(this.name).where(this.primary_key, primary_key).first();
42
+ this.data.set(primary_key, data_created);
43
+ return data_created;
44
+ }
45
+
46
+ async update_data(primary_key, data) {
47
+ const validated_data = this.validate_data(data);
48
+ const check_data = await this.knex(this.name).where(this.primary_key, primary_key).first();
49
+ if(!check_data) {
50
+ throw new Error('Data not found');
51
+ }
52
+ await this.knex(this.name).where(this.primary_key, primary_key).update(validated_data);
53
+ const updated_data = await this.knex(this.name).where(this.primary_key, primary_key).first();
54
+ this.data.set(primary_key, updated_data);
55
+ return updated_data;
56
+ }
57
+
58
+ async delete_data(primary_key) {
59
+ await this.knex(this.name).where(this.primary_key, primary_key).delete();
60
+ this.data.delete(primary_key);
61
+ return true;
62
+ }
63
+
64
+ validate_data(data) {
65
+ const validated_data = {};
66
+ for(const key in this.schema) {
67
+ if(data[key]) {
68
+ validated_data[key] = data[key];
69
+ }
70
+ }
71
+ return validated_data;
72
+ }
73
+ }
@@ -0,0 +1,195 @@
1
+ const BaseStore = require('./base');
2
+
3
+ const stores = {}
4
+
5
+ const add_model = async (name, primary_key, schema) => {
6
+ if(stores[name]) {
7
+ return stores[name];
8
+ }
9
+ stores[name] = new BaseStore(name, primary_key, schema);
10
+ await stores[name].init();
11
+ return stores[name];
12
+ }
13
+
14
+ const get_all = (name, query, reply, routeOptions) => {
15
+
16
+ let { page = 1, limit = 10, search, order } = query;
17
+ const offset = (page - 1) * limit;
18
+
19
+ const store_model = stores[name] || false;
20
+ if(!store_model) {
21
+ return reply.status(404).send({
22
+ status: false,
23
+ message: 'Store not found'
24
+ });
25
+ }
26
+
27
+ let all_data = [...store_model.data.values()];
28
+
29
+ if(search) {
30
+ all_data = all_data.filter(item => {
31
+ return Object.values(item).some(value => value.toString().toLowerCase().includes(search.toLowerCase()));
32
+ });
33
+ }
34
+
35
+ if(order) {
36
+ const splitOrder = order.split(':');
37
+ const orderBy = splitOrder[0];
38
+ const orderDirection = splitOrder[1];
39
+ all_data = all_data.sort((a, b) => {
40
+ if(orderDirection === 'desc') {
41
+ return b[orderBy] - a[orderBy];
42
+ }
43
+ return a[orderBy] - b[orderBy];
44
+ });
45
+ }
46
+
47
+ const filters = routeOptions.filters || {};
48
+ if(Object.keys(filters).length > 0) {
49
+ all_data = all_data.filter(item => {
50
+ return Object.keys(filters).every(key => {
51
+ if(query[key] === undefined) {
52
+ return true;
53
+ }
54
+ let filterStatus = true
55
+ if(filters[key].search_type === 'like') {
56
+ filterStatus = item[key].toString().toLowerCase().includes(query[key].toString().toLowerCase());
57
+ } else if(filters[key].search_type === 'equal') {
58
+ filterStatus = item[key] == query[key];
59
+ } else if(filters[key].search_type === 'in') {
60
+ filterStatus = query[key].includes(item[key]);
61
+ } else if(filters[key].search_type === 'not_in') {
62
+ filterStatus = !query[key].includes(item[key]);
63
+ } else if(filters[key].search_type === 'between') {
64
+ filterStatus = item[key] >= query[key][0] && item[key] <= query[key][1];
65
+ } else if(filters[key].search_type === 'not_between') {
66
+ filterStatus = item[key] < query[key][0] || item[key] > query[key][1];
67
+ }
68
+ return filterStatus;
69
+ });
70
+ });
71
+ }
72
+
73
+ const totalPages = Math.ceil(all_data.length / parseInt(limit));
74
+ if(page > totalPages && page !== 1) {
75
+ return reply.status(404).send({
76
+ status: false,
77
+ message: 'Page not found'
78
+ });
79
+ }
80
+
81
+ return reply.send({
82
+ status: true,
83
+ data: all_data.slice(offset, offset + limit),
84
+ pagination: {
85
+ page: parseInt(page),
86
+ limit: parseInt(limit),
87
+ total: all_data.length,
88
+ totalPages: totalPages
89
+ }
90
+ });
91
+ }
92
+
93
+ const get_by_primary_key = (name, primary_key, reply, routeOptions = {}) => {
94
+
95
+ const store_model = stores[name] || false;
96
+ if(!store_model) {
97
+ return reply.status(404).send({
98
+ status: false,
99
+ message: 'Store not found'
100
+ });
101
+ }
102
+
103
+ const data = store_model.data.get(primary_key);
104
+ if(!data) {
105
+ return reply.status(404).send({
106
+ status: false,
107
+ message: 'Data not found'
108
+ });
109
+ }
110
+
111
+ return reply.send({
112
+ status: true,
113
+ data: data
114
+ });
115
+ }
116
+
117
+ const create_item = async (name, data, reply, routeOptions = {}) => {
118
+
119
+ const store_model = stores[name] || false;
120
+ if(!store_model) {
121
+ return reply.status(404).send({
122
+ status: false,
123
+ message: 'Store not found'
124
+ });
125
+ }
126
+
127
+ try {
128
+ const data_created = await store_model.create_data(data);
129
+ return reply.send({
130
+ status: true,
131
+ data: data_created
132
+ });
133
+ } catch (error) {
134
+ return reply.status(500).send({
135
+ status: false,
136
+ message: error.message
137
+ });
138
+ }
139
+ }
140
+
141
+ const update_item = async (name, primary_key, data, reply, routeOptions = {}) => {
142
+ const store_model = stores[name] || false;
143
+ if(!store_model) {
144
+ return reply.status(404).send({
145
+ status: false,
146
+ message: 'Store not found'
147
+ });
148
+ }
149
+
150
+ try {
151
+ const data_updated = await store_model.update_data(primary_key, data);
152
+ return reply.send({
153
+ status: true,
154
+ data: data_updated
155
+ });
156
+ } catch (error) {
157
+ return reply.status(500).send({
158
+ status: false,
159
+ message: error.message
160
+ });
161
+ }
162
+ }
163
+
164
+ const delete_item = async (name, primary_key, reply, routeOptions = {}) => {
165
+ const store_model = stores[name] || false;
166
+ if(!store_model) {
167
+ return reply.status(404).send({
168
+ status: false,
169
+ message: 'Store not found'
170
+ });
171
+ }
172
+
173
+ try {
174
+ const data_deleted = await store_model.delete_data(primary_key);
175
+ return reply.send({
176
+ status: true,
177
+ data: data_deleted
178
+ });
179
+ } catch (error) {
180
+ return reply.status(500).send({
181
+ status: false,
182
+ message: error.message
183
+ });
184
+ }
185
+ }
186
+
187
+ module.exports = {
188
+ add_model,
189
+ get_all,
190
+ get_by_primary_key,
191
+ create_item,
192
+ update_item,
193
+ delete_item,
194
+ stores
195
+ }
package/utils/i18n.js ADDED
@@ -0,0 +1,187 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Internationalization utility for multi-language support
6
+ */
7
+ class I18n {
8
+ constructor() {
9
+ this.locales = {};
10
+ this.defaultLocale = 'en';
11
+ this.supportedLocales = ['en', 'tr', 'es'];
12
+ this.loadLocales();
13
+ }
14
+
15
+ /**
16
+ * Load all locale files
17
+ */
18
+ loadLocales() {
19
+ const localesPath = path.join(__dirname, '../locales');
20
+
21
+ for (const locale of this.supportedLocales) {
22
+ const localePath = path.join(localesPath, locale);
23
+ if (fs.existsSync(localePath)) {
24
+ this.locales[locale] = {};
25
+
26
+ // Load all JSON files in the locale directory
27
+ const files = fs.readdirSync(localePath);
28
+ for (const file of files) {
29
+ if (file.endsWith('.json')) {
30
+ const filePath = path.join(localePath, file);
31
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
32
+ const key = file.replace('.json', '');
33
+ this.locales[locale][key] = content;
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get translation for a key
42
+ * @param {string} key - Translation key (e.g., 'validation.required')
43
+ * @param {string} locale - Locale code
44
+ * @param {Object} params - Parameters to replace in the translation
45
+ * @returns {string} Translated string
46
+ */
47
+ t(key, locale = this.defaultLocale, params = {}) {
48
+ const keys = key.split('.');
49
+ let translation = this.locales[locale] || this.locales[this.defaultLocale];
50
+
51
+ // Navigate through nested keys
52
+ for (const k of keys) {
53
+ if (translation && translation[k]) {
54
+ translation = translation[k];
55
+ } else {
56
+ // Fallback to default locale
57
+ translation = this.locales[this.defaultLocale];
58
+ for (const k of keys) {
59
+ if (translation && translation[k]) {
60
+ translation = translation[k];
61
+ } else {
62
+ return key; // Return key if translation not found
63
+ }
64
+ }
65
+ break;
66
+ }
67
+ }
68
+
69
+ // Replace parameters in the translation
70
+ if (typeof translation === 'string') {
71
+ return this.replaceParams(translation, params);
72
+ }
73
+
74
+ return key;
75
+ }
76
+
77
+ /**
78
+ * Replace parameters in translation string
79
+ * @param {string} str - String with parameters
80
+ * @param {Object} params - Parameters to replace
81
+ * @returns {string} String with replaced parameters
82
+ */
83
+ replaceParams(str, params) {
84
+ return str.replace(/\{(\w+)\}/g, (match, key) => {
85
+ return params[key] !== undefined ? params[key] : match;
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Detect language from request headers
91
+ * @param {Object} request - Fastify request object
92
+ * @returns {string} Detected locale
93
+ */
94
+ detectLanguage(request) {
95
+ // Check Accept-Language header
96
+ const acceptLanguage = request.headers['locale'] || request.headers['Accept-Language'];
97
+ if (acceptLanguage) {
98
+ const languages = acceptLanguage.split(',').map(lang => {
99
+ const [locale, quality] = lang.trim().split(';q=');
100
+ return {
101
+ locale: locale.split('-')[0], // Get language part only
102
+ quality: quality ? parseFloat(quality) : 1.0
103
+ };
104
+ });
105
+
106
+ // Sort by quality and find first supported locale
107
+ languages.sort((a, b) => b.quality - a.quality);
108
+ for (const lang of languages) {
109
+ if (this.supportedLocales.includes(lang.locale)) {
110
+ return lang.locale;
111
+ }
112
+ }
113
+ }
114
+
115
+ // Check custom header
116
+ const customLang = request.headers['locale'];
117
+ if (customLang && this.supportedLocales.includes(customLang)) {
118
+ return customLang;
119
+ }
120
+
121
+ // Check query parameter
122
+ const queryLang = request.query?.lang;
123
+ if (queryLang && this.supportedLocales.includes(queryLang)) {
124
+ return queryLang;
125
+ }
126
+
127
+ return this.defaultLocale;
128
+ }
129
+
130
+ /**
131
+ * Get validation error message
132
+ * @param {Object} error - Ajv validation error
133
+ * @param {string} locale - Locale code
134
+ * @returns {string} Localized error message
135
+ */
136
+ getValidationErrorMessage(error, locale = this.defaultLocale) {
137
+ const { keyword, instancePath, params, message } = error;
138
+ const field = instancePath.replace('/', '') || 'field';
139
+
140
+ let errorKey = `validation.${keyword}`;
141
+
142
+ // Special cases for common validation errors
143
+ if (keyword === 'minLength') {
144
+ errorKey = 'validation.minLength';
145
+ } else if (keyword === 'maxLength') {
146
+ errorKey = 'validation.maxLength';
147
+ } else if (keyword === 'minimum') {
148
+ errorKey = 'validation.min';
149
+ } else if (keyword === 'maximum') {
150
+ errorKey = 'validation.max';
151
+ } else if (keyword === 'format' && params?.format === 'email') {
152
+ errorKey = 'validation.invalidEmail';
153
+ } else if (keyword === 'enum') {
154
+ errorKey = 'validation.enum';
155
+ }
156
+
157
+ const errorParams = {
158
+ field,
159
+ ...params,
160
+ enum: params?.allowedValues ? params.allowedValues.join(', ') : ''
161
+ };
162
+
163
+ return this.t(errorKey, locale, errorParams);
164
+ }
165
+
166
+ /**
167
+ * Get all supported locales
168
+ * @returns {Array} Array of supported locale codes
169
+ */
170
+ getSupportedLocales() {
171
+ return this.supportedLocales;
172
+ }
173
+
174
+ /**
175
+ * Check if locale is supported
176
+ * @param {string} locale - Locale code
177
+ * @returns {boolean} True if supported
178
+ */
179
+ isLocaleSupported(locale) {
180
+ return this.supportedLocales.includes(locale);
181
+ }
182
+ }
183
+
184
+ // Create singleton instance
185
+ const i18n = new I18n();
186
+
187
+ module.exports = i18n;