From 044a282a962d735ad9800b577394db92c5aea0c0 Mon Sep 17 00:00:00 2001 From: shamoo53 Date: Sun, 22 Feb 2026 12:36:08 +0100 Subject: [PATCH] Database-Organization-Metadata Database-Organization-Metadata --- .../002_create_organizations_table.sql | 20 +++++ backend/src/index.js | 76 ++++++++--------- backend/src/models/index.js | 4 +- backend/src/models/organization.js | 60 +++++++++++++ backend/src/models/vault.js | 39 ++++++++- backend/src/swagger/swaggerConfig.js | 85 +++++++++++++++++++ backend/test/organization.test.js | 80 +++++++++++++++++ 7 files changed, 320 insertions(+), 44 deletions(-) create mode 100644 backend/migrations/002_create_organizations_table.sql create mode 100644 backend/src/models/organization.js create mode 100644 backend/test/organization.test.js diff --git a/backend/migrations/002_create_organizations_table.sql b/backend/migrations/002_create_organizations_table.sql new file mode 100644 index 00000000..50ca5ae8 --- /dev/null +++ b/backend/migrations/002_create_organizations_table.sql @@ -0,0 +1,20 @@ +-- Create organizations table +CREATE TABLE IF NOT EXISTS organizations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + logo_url VARCHAR(500), + website_url VARCHAR(500), + discord_url VARCHAR(500), + admin_address VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create index on admin_address for faster lookups +CREATE INDEX idx_organizations_admin_address ON organizations(admin_address); + +-- Create index on created_at for sorting +CREATE INDEX idx_organizations_created_at ON organizations(created_at); + +-- Add comment to the table +COMMENT ON TABLE organizations IS 'Stores off-chain metadata about organizations that create vaults'; \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js index b5bdd286..ea450c54 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -218,47 +218,45 @@ app.get('/api/admin/pending-transfers', async (req, res) => { } }); - - res.status(500).json({ - success: false, - error: error.message - }); - } -}); - - - res.status(500).json({ - success: false, - error: error.message - }); - } -}); - - - res.status(500).json({ - success: false, - error: error.message - }); - } -}); - -< - res.status(500).json({ - success: false, - error: error.message +// Organization Routes +app.get('/api/org/:address', async (req, res) => { + try { + const { address } = req.params; + + // Validate address format (basic validation) + if (!address || address.length < 20) { + return res.status(400).json({ + success: false, + error: 'Valid admin address required' + }); + } + + const organization = await models.Organization.findOne({ + where: { admin_address: address } }); - } -}); - - - res.status(500).json({ - success: false, - error: error.message + + if (!organization) { + return res.status(404).json({ + success: false, + error: 'Organization not found for this admin address' + }); + } + + res.json({ + success: true, + data: { + id: organization.id, + name: organization.name, + logo_url: organization.logo_url, + website_url: organization.website_url, + discord_url: organization.discord_url, + admin_address: organization.admin_address, + created_at: organization.created_at, + updated_at: organization.updated_at + } }); - } -}); - - + } catch (error) { + console.error('Error fetching organization:', error); res.status(500).json({ success: false, error: error.message diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 49ad4a8f..ecf4ae42 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -2,17 +2,19 @@ const { sequelize } = require('../database/connection'); const ClaimsHistory = require('./claimsHistory'); const Vault = require('./vault'); const SubSchedule = require('./subSchedule'); +const Organization = require('./organization'); const models = { ClaimsHistory, Vault, SubSchedule, + Organization, sequelize, }; -// Setup associations if needed in the future +// Setup associations Object.keys(models).forEach((modelName) => { if (models[modelName].associate) { models[modelName].associate(models); diff --git a/backend/src/models/organization.js b/backend/src/models/organization.js new file mode 100644 index 00000000..4b9db643 --- /dev/null +++ b/backend/src/models/organization.js @@ -0,0 +1,60 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../database/connection'); + +const Organization = sequelize.define('Organization', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + logo_url: { + type: DataTypes.STRING, + allowNull: true, + }, + website_url: { + type: DataTypes.STRING, + allowNull: true, + }, + discord_url: { + type: DataTypes.STRING, + allowNull: true, + }, + admin_address: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, +}, { + tableName: 'organizations', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + indexes: [ + { + fields: ['admin_address'], + unique: true, + }, + ], +}); + +// Add association method +Organization.associate = function(models) { + Organization.hasMany(models.Vault, { + foreignKey: 'org_id', + as: 'vaults' + }); +}; + +module.exports = Organization; \ No newline at end of file diff --git a/backend/src/models/vault.js b/backend/src/models/vault.js index d9db661b..ad506b05 100644 --- a/backend/src/models/vault.js +++ b/backend/src/models/vault.js @@ -7,13 +7,35 @@ const Vault = sequelize.define('Vault', { defaultValue: DataTypes.UUIDV4, primaryKey: true, }, - + address: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + name: { + type: DataTypes.STRING, + allowNull: true, + }, + token_address: { + type: DataTypes.STRING, + allowNull: false, + }, + owner_address: { + type: DataTypes.STRING, + allowNull: false, }, total_amount: { type: DataTypes.DECIMAL(36, 18), allowNull: false, defaultValue: 0, - + }, + org_id: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'organizations', + key: 'id' + } }, created_at: { type: DataTypes.DATE, @@ -30,15 +52,24 @@ const Vault = sequelize.define('Vault', { updatedAt: 'updated_at', indexes: [ { -n + fields: ['address'], unique: true, }, { fields: ['owner_address'], }, { - + fields: ['org_id'], + } ], }); +// Add association method +Vault.associate = function(models) { + Vault.belongsTo(models.Organization, { + foreignKey: 'org_id', + as: 'organization' + }); +}; + module.exports = Vault; diff --git a/backend/src/swagger/swaggerConfig.js b/backend/src/swagger/swaggerConfig.js index 9920f515..c945c4c7 100644 --- a/backend/src/swagger/swaggerConfig.js +++ b/backend/src/swagger/swaggerConfig.js @@ -979,4 +979,89 @@ * $ref: '#/components/schemas/ErrorResponse' */ +/** + * @swagger + * /api/org/{address}: + * get: + * summary: Get organization info by admin address + * description: Retrieves organization metadata (logo, website, discord) by admin address + * tags: [Organization] + * parameters: + * - in: path + * name: address + * required: true + * schema: + * type: string + * description: Admin address to retrieve organization info for + * responses: + * 200: + * description: Organization info retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * id: + * type: string + * description: Organization ID + * name: + * type: string + * description: Organization name + * logo_url: + * type: string + * description: URL to organization logo + * website_url: + * type: string + * description: URL to organization website + * discord_url: + * type: string + * description: URL to organization Discord + * admin_address: + * type: string + * description: Admin address linked to this organization + * created_at: + * type: string + * format: date-time + * description: Creation timestamp + * updated_at: + * type: string + * format: date-time + * description: Last update timestamp + * example: + * success: true + * data: + * id: "550e8400-e29b-41d4-a716-446655440000" + * name: "Acme Corp" + * logo_url: "https://acme.com/logo.png" + * website_url: "https://acme.com" + * discord_url: "https://discord.gg/acme" + * admin_address: "0x1234567890123456789012345678901234567890" + * created_at: "2024-01-01T00:00:00.000Z" + * updated_at: "2024-01-01T00:00:00.000Z" + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * 404: + * description: Organization not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ + module.exports = {}; \ No newline at end of file diff --git a/backend/test/organization.test.js b/backend/test/organization.test.js new file mode 100644 index 00000000..400ac37c --- /dev/null +++ b/backend/test/organization.test.js @@ -0,0 +1,80 @@ +const { sequelize, models } = require('../src/models'); + +async function testOrganizationModel() { + console.log('๐Ÿงช Testing Organization Model Implementation\n'); + + try { + // Sync the database (this will create the tables if they don't exist) + await sequelize.sync({ force: false }); // Use force: false to avoid dropping existing data + + console.log('โœ… Database synced successfully'); + + // Test 1: Create an organization + console.log('\n1. Testing organization creation...'); + + const testOrg = await models.Organization.create({ + name: 'Test Organization', + logo_url: 'https://example.com/logo.png', + website_url: 'https://example.com', + discord_url: 'https://discord.gg/example', + admin_address: '0x1234567890123456789012345678901234567890' + }); + + console.log('โœ… Organization created:', testOrg.toJSON()); + + // Test 2: Find the organization by admin address + console.log('\n2. Testing organization retrieval by admin address...'); + + const foundOrg = await models.Organization.findOne({ + where: { admin_address: '0x1234567890123456789012345678901234567890' } + }); + + console.log('โœ… Organization retrieved:', foundOrg.toJSON()); + + // Test 3: Create a vault and link it to the organization + console.log('\n3. Testing vault creation with organization link...'); + + const testVault = await models.Vault.create({ + address: '0xabcdef1234567890abcdef1234567890abcdef1234', + name: 'Test Vault', + token_address: '0xdef1234567890abcdef1234567890abcdef12345', + owner_address: '0x1234567890123456789012345678901234567890', + total_amount: '1000', + org_id: foundOrg.id + }); + + console.log('โœ… Vault created with organization link:', testVault.toJSON()); + + // Test 4: Test the association - get vaults for the organization + console.log('\n4. Testing organization-vault association...'); + + const orgWithVaults = await models.Organization.findByPk(foundOrg.id, { + include: [{ + model: models.Vault, + as: 'vaults' + }] + }); + + console.log('โœ… Organization with associated vaults:', { + org: orgWithVaults.toJSON(), + vaults: orgWithVaults.vaults.map(v => v.toJSON()) + }); + + console.log('\n๐ŸŽ‰ All organization model tests passed!'); + + // Clean up - delete the test data (optional) + // await testVault.destroy(); + // await testOrg.destroy(); + + } catch (error) { + console.error('โŒ Test failed:', error.message); + process.exit(1); + } +} + +// Run tests if this file is executed directly +if (require.main === module) { + testOrganizationModel(); +} + +module.exports = { testOrganizationModel }; \ No newline at end of file