window.pipedriveLeadboosterConfig = { basis: 'leadbooster-chat.pipedrive.com', companyId: 11580370, playbookUuid: '22236db1-6d50-40c4-b48f-8b11262155be', versie: 2, } ;(functie () { var w = venster als (w.LeadBooster) { console.warn('LeadBooster bestaat al') } anders { w.LeadBooster = { q: [], on: functie (n, h) { this.q.push({ t: 'o', n: n, h: h }) }, trigger: functie (n) { this.q.push({ t: 't', n: n }) }, } } })() GraphQL/MongoDB API implementeren met Netlify Functies - The Codest
The Codest
  • Over ons
  • Diensten
    • Software Ontwikkeling
      • Frontend ontwikkeling
      • Backend ontwikkeling
    • Staff Augmentation
      • Frontend ontwikkelaars
      • Backend ontwikkelaars
      • Gegevensingenieurs
      • Cloud Ingenieurs
      • QA ingenieurs
      • Andere
    • Het advies
      • Audit & Consulting
  • Industrie
    • Fintech & Bankieren
    • E-commerce
    • Adtech
    • Gezondheidstechnologie
    • Productie
    • Logistiek
    • Automotive
    • IOT
  • Waarde voor
    • CEO
    • CTO
    • Leveringsmanager
  • Ons team
  • Case Studies
  • Weten hoe
    • Blog
    • Ontmoetingen
    • Webinars
    • Bronnen
Carrière Neem contact op
  • Over ons
  • Diensten
    • Software Ontwikkeling
      • Frontend ontwikkeling
      • Backend ontwikkeling
    • Staff Augmentation
      • Frontend ontwikkelaars
      • Backend ontwikkelaars
      • Gegevensingenieurs
      • Cloud Ingenieurs
      • QA ingenieurs
      • Andere
    • Het advies
      • Audit & Consulting
  • Waarde voor
    • CEO
    • CTO
    • Leveringsmanager
  • Ons team
  • Case Studies
  • Weten hoe
    • Blog
    • Ontmoetingen
    • Webinars
    • Bronnen
Carrière Neem contact op
Pijl terug KEREN TERUG
2021-05-13
Software Ontwikkeling

GraphQL/MongoDB API implementeren met Netlify Functies

The Codest

Pawel Rybczynski

Software Engineer

Doelen Eerste installatie Dependencies installeren Laten we beginnen Voeg eerst tsconfig.json toe aan de hoofddirectory: Laten we nu src/server.ts maken voor serverimplementaties. Voeg dan twee functies toe: een voor de lokale server en een voor de lambda. OK, we hebben geen resolvers of type definities dus we moeten er een paar maken. Laten we aannemen dat we in eerste instantie [...]

Doelen

  1. Configureer zowel lokale als lambda-servers.
  2. Verbind beide met MongoDB.
  3. Basisauthenticatie implementeren.
  4. Serverloze Apollo implementeren GraphQL API met Netlify.
  5. Typescript gebruiken.

Eerste installatie

npm init -y

Afhankelijkheden installeren

npm install --save typescript graphql aws-lambda @types/aws-lambda

Laten we beginnen

Voeg eerst de tsconfig.json naar de hoofdmap:

 {
 "compilerOptions": {
 "doel": "es5",
 "module": "commonjs
 "allowJs": true,
 "strict": true,
 "esModuleInterop": true,
 "skipLibCheck": true,
 "forceConsistentCasingInFileNames": true
 },
 "include": ["src/*.ts", "src/**/*.ts", "src/**/*.js"],
 "exclude": ["node_modules"].
 }

Laten we nu src/server.ts voor serverimplementaties. Voeg dan twee functies toe: een voor lokale server en een voor lambda.

// src/server.ts
import { ApolloServer as ApolloServerLambda } from "apollo-server-lambda";
import { ApolloServer } from "apollo-server";


const createLambdaServer = () =>
  nieuwe ApolloServerLambda({
    typeDefs,
    resolvers,
    introspectie: true,
    speeltuin: waar,
    },
  });

const createLocalServer = () =>
  nieuwe ApolloServer({
    typeDefs,
    resolvers,
    introspectie: true,
    speeltuin: waar,
    },
  });

export { createLambdaServer, createLocalServer };

OK, we hebben geen resolvers of typedefinities dus we moeten er een paar maken. Laten we aannemen dat we eerst gebruikers willen aanmaken en informatie over hen willen ontvangen.

// src/schemas.ts
const { gql } = require("apollo-server-lambda");

const userSchema = gql`
  type Gebruiker {
    id: ID!
    email: String!
    naam: String!
  }

  type Query {
    user(id: ID!): Gebruiker!
  }

  type Mutatie {
    createUser(naam: String!, e-mail: String!, wachtwoord: String!): Gebruiker!
  }
`;

Als je hier niet bekend mee bent, Apollo heeft een erg leuke tutorial voorbereid

Laten we nu een user resolver maken met één query en één mutatie.

// src/resolvers.ts
const userResolver = {
  Query: {
    user: async (parent, args, context, info) => {
      {...}
    },
  },
  Mutatie: {
    createUser: async (parent, args, context, info) => {
      {...}
    },
  },
};

Maar we hebben geen gegevens... Laten we het oplossen 😉

Vergeet niet om de typedefinitie en resolver te importeren op de server.

// src/server.ts
import { ApolloServer as ApolloServerLambda } from "apollo-server-lambda";
import { ApolloServer } from "apollo-server";

import { typeDefs } from "./schemas";
import { resolvers } from "./resolvers";

{...}

Verbinding maken met MongoDB via mongoose

Nu is het een goed moment om een verbinding te maken met onze database. In dit specifieke geval zal dat MongoDB zijn. Het is gratis en gemakkelijk te onderhouden. Maar laten we eerst nog twee afhankelijkheden installeren:

npm install --save mongoose dotenv

De eerste stap is het maken van een gebruikersmodel.

// src/model.ts
importeer mongoose, { Document, Error, Schema } van "mongoose";

export type User = Document & {
  _id: string,
  email: string,
  naam: tekenreeks,
  wachtwoord: tekenreeks,
};

verwijderen mongoose.connection.models["User"];

const UserSchema: Schema = nieuw Schema({
  e-mail: {
    type: String,
    verplicht: waar,
    uniek: waar,
  },
  naam: {
    type: String,
    verplicht: waar,
    minLength: 3,
    maxLength: 32,
  },
  wachtwoord: {
    type: String,
    verplicht: waar,
  },
});

export const userModel = mongoose.model  ("User", UserSchema);

Wachtwoorden veiliger maken

Veiligheid eerst! Laten we nu onze wachtwoorden beveiligen door ze te hashen.

npm install --save bcrypt @types/bcrypt

Implementeer nu de wachtwoordbeveiliging in de pre-middleware callback. Pre-middleware functies worden na elkaar uitgevoerd, wanneer elke middleware de volgende aanroept. Om de wachtwoorden veilig te maken, gebruiken we een techniek die een salt en hash genereert bij afzonderlijke functie-aanroepen.

// src/model.ts
importeer bcrypt van "bcrypt";
{...}
const SALT_WORK_FACTOR: getal = 10;

GebruikersSchema.pre("opslaan", functie (volgende) {
  const user = this as User;
  if (!this.isModified("password")) return next();

  bcrypt.genSalt(SALT_WORK_FACTOR, functie (err, salt) {
    if (err) return next(err);

    bcrypt.hash(user.password, salt, functie (err, hash) {
      if (err) return next(err);
      user.password = hash;
      next();
    });
  });
});
{...}

Voeg dan de methode ComparePasswords toe aan UserSchema:

// src/model.ts
{...}
UserSchema.methods.comparePasswords = functie (
  kandidaatWachtwoord: string,
  cb: (err: Error | null, same: boolean | null) => void
) {
  const user = this as User;
  bcrypt.compare(candidatePassword, user.password, (err, isMatch) => {
    if (err) {
      return cb(err, null);
    }
    cb(null, isMatch);
  });
};
{...}

En natuurlijk het gebruikerstype wijzigen.

type comparePasswordFunction = (
  kandidaatWachtwoord: string,
  cb: (err: fout, isMatch: booleaans) => void
) => void;

export type User = Document & {
  _id: string,
  e-mail: tekenreeks,
  naam: tekenreeks,
  wachtwoord: tekenreeks,
  comparePasswords: comparePasswordFunction,
};

Nu kunnen we een verbinding regelen tussen servers en databases. MONGODB_URI is een omgevingsvariabele die een verbindingstekenreeks bevat die nodig is om de verbinding te maken. Je kunt deze uit je clusterpaneel halen nadat je bent ingelogd op je MongoDB atlas account. Zet het in .env

// .env
MONGODB_URI = ...;

Vergeet niet om dat bestand altijd toe te voegen aan .gitignore. Geweldig! Laten we nu een functie toevoegen waarmee we verbinding kunnen maken met db.

// src/server.ts
importeer mongoose, { Connection } van "mongoose";
{...}
laat cachedDb: Connection;

const connectToDatabase = async () => {
  if (cachedDb) return;

  await mongoose.connect(process.env.MONGODB_URI || "", {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useFindAndModify: false,
    useCreateIndex: true,
  });
  cachedDb = mongoose.connection;
};
{...}

De context is een object dat wordt gedeeld door alle resolvers. Om het aan te bieden, hoeven we alleen maar een context initialisatiefunctie toe te voegen aan de ApolloServer constructor. Laten we dat doen.

// src/server.ts
importeer { userModel } uit "./models/user.model";
{...}
const createLambdaServer = async () =>
  nieuwe ApolloServerLambda({
    typeDefs,
    resolvers,
    introspectie: true,
    speeltuin: waar,
    context: async () => {
      await connectToDatabase();

      return {
        modellen: {
          userModel,
        },
      };
    },
  });

const createLocalServer = () =>
  nieuwe ApolloServer({
    typeDefs,
    resolvers,
    introspectie: true,
    speeltuin: waar,
    context: async () => {
      await connectToDatabase();

      return {
        modellen: {
          userModel,
        },
      };
    }
  });

Zoals je kunt zien, geven we ook gebruikersmodel via context. We kunnen nu de resolver bijwerken:

// resolvers.ts
const userResolver = {
  Query: {
    user: async (_, { email, naam, wachtwoord }, { models: { userModel } }) => {
      const user = await userModel.findById({ _id: id }).exec();
      user terug;
    },
  },
  Mutatie: {
    createUser: async (_, { id }, { models: { userModel } }) => {
      const user = await userModel.create({ email, naam, wachtwoord });
      gebruiker terug;
    },
  },
};

Ziet er goed uit! Maak nu een basis-serverinstantie:

// src/index.ts
importeer { createLocalServer } van "./server";
require("dotenv").config();

const poort = process.env.PORT || 4000;

const server = createLocalServer();

server.listen(port).then(({url }) => {
  console.log(`Server ir draait op ${url}`);
});

De lokale server starten met Nodemon

Voeg als laatste voor de run nodemon.json

{
  "watch": ["src"],
  "ext": ".ts,.js",
  "ignore": [],
  "exec": "ts-node --transpile-only ./src/index.ts"
}

Script toevoegen aan pakket.json

"scripts": {
    "start": "nodemon"
  },

En rennen!

npm start

Je zou dergelijke informatie in de terminal moeten krijgen:

Server ir draait op http://localhost:4000/

Zo ja, open dan de http://localhost:4000/ in uw browser.

De GraphQL speeltuin zou moeten verschijnen. Laten we een nieuwe gebruiker aanmaken!

gebruiker aanmaken.png

Kijk eens hoe het eruit ziet in de database.

databaseJohnDoe.png

Gaaf! Alles werkt prima!

Laten we proberen wat gebruikersinformatie te krijgen.

gebruikerzonderauthenticatie.png

En het wachtwoordveld toevoegen...

gebruikersWachtwoord.png

Leuk! We ontvangen foutmelding Kan veld "wachtwoord" niet bevragen op type "Gebruiker".". Zoals je kunt zien, hebben we dit veld niet toegevoegd aan de definitie van het gebruikerstype. Het is er met opzet. We moeten het niet mogelijk maken om een wachtwoord of andere gevoelige gegevens op te vragen.

Nog iets... We kunnen gebruikersgegevens krijgen zonder authenticatie... het is geen goede oplossing. We moeten dit oplossen.

Maar voordat...

Codegen configureren

Laten we GraphQL gebruiken code generator om een compatibel basistype te krijgen, gebaseerd op ons schema.

npm install --save @graphql-codegen/cli @graphql-codegen/introspection
@graphql-codegen/typescript @graphql-codegen/typescript-resolvers

Maak codegen.yml

overschrijven: waar
schema: "http://localhost:4000"
genereert:
  ./src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-resolvers"
  ./graphql.schema.json:
    plugins:
      - "introspection"

Voeg toe codegen script naar pakket.json

"scripts": {
    "start": "nodemon",
    "codegen": "graphql-codegen --config ./codegen.yml",
  },

Voer vervolgens het script uit wanneer de lokale server draait:

npm uitvoeren codegen

Bij succes krijg je een bericht:

  √ Configuratie ontleden
  √ Uitvoer genereren

Als je die info ontvangt, zouden er twee bestanden moeten verschijnen:

  • grafql.schema.json in de hoofdmap
  • grafql.ts in nieuw aangemaakt pad src/gegenereerd

We zijn meer geïnteresseerd in de tweede. Als je die opent, zie je een mooie structuur van types.

Nu kunnen we onze oplossers verbeteren:

// src/resolvers.ts
import { Resolvers, Token, User } from "./generated/graphql";

const userResolver: Resolvers = {
  Query: {
    user: async (_, { id }, { models: { userModel }, auth }): Promise => {
      const user = await userModel.findById({ _id: id }).exec();
      gebruiker terug;
    },
  },
  Mutatie: {
    createUser: async (
      _,
      { email, naam, wachtwoord },
      {modellen: { userModel } }
    ): Promise => {
      const user = await userModel.create({
        e-mail,
        naam,
        wachtwoord,
      });
      gebruiker terug;
    },
  },
};

Authenticatie

Laten we vervolgens een eenvoudige token-gebaseerde authenticatie instellen.

npm install --save jsonwebtoken @types/jsonwebtoken

Maak checkAuth functie om te controleren of het token geldig is. We voegen het resultaat toe aan de context zodat we er toegang toe hebben in de resolvers.

// src/server.ts
import { IncomingHttpHeaders } from "http";
import {
  APIGatewayProxyEvent,
  APIGatewayProxyEventHeaders,
  Context,
} from "aws-lambda";
import jwt from "jsonwebtoken";
{...}
const checkAuth = async ({ token } : APIGatewayProxyEventHeaders | IncomingHttpHeaders ) => {
  if (typeof token === "string") {
    try {
      return await jwt.verify(token, "riddlemethis");
    } catch (e) {
      throw new AuthenticationError(`Your session expired. Sign in again.`);
    }
  }
};
{...}
const createLambdaServer = async (
  { headers }: APIGatewayProxyEvent,
  context: Context
) => {
  return new ApolloServerLambda({
    typeDefs,
    resolvers,
    context: async () => {
      await connectToDatabase();

      const auth = await checkAuth(headers);

      return {
        auth,
        models: {
          userModel,
        },
      };
    },
  });
};

function createLocalServer() {
  return new ApolloServer({
    typeDefs,
    resolvers,
    introspection: true,
    playground: true,
    context: async ({ req: { headers } = {} }) => {
      const auth = await checkAuth(headers);
      await connectToDatabase();

      return {
         auth,
         models: {
          userModel,
        },
      };
    },
  });
}

We hebben ook een manier nodig om zo'n token aan te maken. De beste manier is om een aanmeldingsquery in onze resolver te implementeren.

// resolvers.ts
import { Resolvers, Token, User } from "./generated/graphql";

const userResolver: Resolvers = {
  Query: {
    user: async (_, { id }, { models: { userModel }, auth }): Promise => {
      if (!auth) throw new AuthenticationError("U bent niet geauthenticeerd");

      const user = await userModel.findById({ _id: id }).exec();
      gebruiker terug;
    },
    login: async (
      _,
      { email, wachtwoord },
      {modellen: { userModel } }
    ): Belofte => {
      const user = await userModel.findOne({ email }).exec();

      if (!user) throw new AuthenticationError("Invalid credentials");

      const matchPasswords = bcrypt.compareSync(password, user.password);

      Als (!matchPasswords) gooi dan de nieuwe AuthenticationError("Ongeldige aanmeldingsgegevens");

      const token = jwt.sign({ id: user.id }, "riddlemethis", {
        verlooptIn: 60,
      });

      return { token };
    },
  },
  Mutatie: {
    createUser: async (
      _,
      { email, naam, wachtwoord },
      {modellen: { userModel } }
    ): Promise => {
      const user = await userModel.create({
        e-mail,
        naam,
        wachtwoord,
      });
      gebruiker terug;
    },
  },
};

U moet ook definities van gebruikerstypes bijwerken per Token-type en aanmeldingsquery.

type token {
    token: String.
  }

type Query {
    user(id: ID!): Gebruiker!
    login(email: String!, wachtwoord: String!): Token!
  }

Laten we nu proberen een gebruiker te krijgen zonder token

noAuth.png

OK, werkt prima! Probeer in te loggen

token.png

Laten we het opnieuw proberen om de gebruiker te krijgen, maar deze keer met een token toegevoegd aan de headers

metkenmerk.png

Gaaf! En wat als we verkeerde referenties instellen?

verkeerdeEmail.png

Leuk!

Voorbereiden op implementatie

Laatste stap: serverloze api implementeren met Netlify!

Map maken lambda in main dir en zet er twee bestanden in:

Bevat eerst AWS handler. Er wordt een ApolloServerLambda-serverinstantie gemaakt en
stel dan een handler bloot met createHandler van die instantie.

// lambda/graphql.ts
import { APIGatewayProxyEvent, Context } from "aws-lambda";
import { createLambdaServer } from "???";

export const handler = async (
  event: APIGatewayProxyEvent,
  context: Context
) => {
  const server = await createLambdaServer(event, context);

  return new Promise((res, rej) => {
    const cb = (err: Error, args: any) => (err ? rej(err) : res(args));
    server.createHandler()(event, context, cb);
  });
};

Je kunt er meer over lezen.

De tweede is tsconfig. Belangrijk onderdeel is de outDir veld.

// lambda/tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true
    "noImplicitAny": true,
    "module": "commonjs
    "target": "es6",
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "knooppunt",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "outDir": "./dist"
  },
  "include": ["./*.ts", "./**/*.ts", "./**/*.js"].
}

Maar er is een probleem. We kunnen de /bron dir omdat Netlify niet buiten de lambda folder kan komen. Dus moeten we het bundelen.

npm install --save ncp

Het is een pakket waarmee je mappen kunt kopiëren.

Voeg toe bundel script naar pakket.json

"scripts": {
    "bundle": "ncp ./src ./lambda/bundle",
    "codegen": "graphql-codegen --config ./codegen.yml",
    "start": "nodemon",
  },

Als je nu npm bundel uitvoerenkun je zien dat in nieuw aangemaakte lambda/bundel dir hebben we alle bestanden van src/.

Werk het importpad bij in lambda/graphql.ts

// lambda/graphql.ts
import { APIGatewayProxyEvent, Context } from "aws-lambda";
import { createLambdaServer } from "./bundle/server";

{...}

Je kunt nu lambda/bundle dir toevoegen aan .gitignore

Uitrollen met Netlify

We moeten Netlify vertellen wat het build commando is en waar onze functies staan. Laten we daartoe netlify.toml file:

// netlify.toml
[build]
  commando = "npm run build:lambda"
  functies = "lambda/dist"

Zoals je kunt zien, is het dezelfde map als gedefinieerd als een outDir veld in lambda/tsconfig.json

Zo zou je app-structuur eruit moeten zien (nou ja... een deel ervan ;))

app
└───lambda
Handleiding
│ │ graphql.ts
│ tsconfig.json.ts
└───src
│ └───generated
│ │ graphql.ts
Index.ts
│ │ model.ts
│ │ resolvers.ts
│ │ schema's.ts
server.ts
│
codegen.yml
│ graphql.schema.json
netlify.toml
nodemon.json
tsconfig.json

Voeg toe bundel:lambda script naar pakket.json

"scripts": {
    "build:lambda": "npm run bundle && tsc -p lambda/tsconfig.json",
    "bundle": "ncp ./src ./lambda/bundle",
    "codegen": "graphql-codegen --config ./codegen.yml",
    "start": "nodemon",
  },

Installeer

Laten we proberen onze app te implementeren via Netlify.

  1. Log in op je Netlify account,
  2. Maak verbinding met je Github,
  3. Klik op de knop 'Nieuwe site van Git',
  4. Selecteer een juiste repo,
  5. Stel de bouwopdracht npm run build:lambda in,
  6. Omgevingsvariabele (MONGODB_URI) toevoegen,
  7. Inzetten...

En TAAADAAAA...

pageNotFound.png

Dat komt omdat het eindpunt standaard niet het http://page/ maar http://page/.netlify/functions/graphql.

zonderomleiding.png

Hoe los je het op? Dat is heel eenvoudig. Maak gewoon omleidingen met:

/.netlify/functions/graphql 200!

Implementeer opnieuw en controleer.

omleiden.png

Ik hoop dat je het leuk vond! Voel je vrij om te verbeteren en te veranderen.

Lees meer:

Hoe help je een project niet om zeep met slechte codeerpraktijken?

Beveiliging van webapps. Target="_blank" kwetsbaarheid

Beveiliging van webapps - XSS-kwetsbaarheid

Verwante artikelen

Software Ontwikkeling

Bouw Toekomstbestendige Web Apps: Inzichten van The Codest's Expert Team

Ontdek hoe The Codest uitblinkt in het creëren van schaalbare, interactieve webapplicaties met geavanceerde technologieën, het leveren van naadloze gebruikerservaringen op alle platforms. Ontdek hoe onze expertise digitale transformatie en business...

DE BESTE
Software Ontwikkeling

Top 10 in Letland gevestigde bedrijven voor softwareontwikkeling

Lees meer over de beste softwareontwikkelingsbedrijven van Letland en hun innovatieve oplossingen in ons nieuwste artikel. Ontdek hoe deze technologieleiders uw bedrijf kunnen helpen verbeteren.

thecodest
Oplossingen voor ondernemingen en schaalvergroting

Essentiële Java-softwareontwikkeling: Een gids voor succesvol uitbesteden

Verken deze essentiële gids over succesvolle outsourcing Java-softwareontwikkeling om de efficiëntie te verbeteren, toegang te krijgen tot expertise en projectsucces te stimuleren met The Codest.

thecodest
Software Ontwikkeling

De ultieme gids voor outsourcing in Polen

De sterke groei van outsourcing in Polen wordt gedreven door economische, educatieve en technologische vooruitgang, die IT-groei en een bedrijfsvriendelijk klimaat stimuleert.

DeCodest
Oplossingen voor ondernemingen en schaalvergroting

De complete gids voor IT-auditmiddelen en -technieken

IT-audits zorgen voor veilige, efficiënte en compliant systemen. Lees het volledige artikel om meer te weten te komen over het belang ervan.

The Codest
Jakub Jakubowicz CTO & medeoprichter

Abonneer je op onze kennisbank en blijf op de hoogte van de expertise uit de IT-sector.

    Over ons

    The Codest - Internationaal softwareontwikkelingsbedrijf met technische hubs in Polen.

    Verenigd Koninkrijk - Hoofdkantoor

    • Kantoor 303B, 182-184 High Street North E6 2JA
      Londen, Engeland

    Polen - Lokale technologieknooppunten

    • Fabryczna kantorenpark, Aleja
      Pokoju 18, 31-564 Krakau
    • Hersenambassade, Konstruktorska
      11, 02-673 Warschau, Polen

      The Codest

    • Home
    • Over ons
    • Diensten
    • Case Studies
    • Weten hoe
    • Carrière
    • Woordenboek

      Diensten

    • Het advies
    • Software Ontwikkeling
    • Backend ontwikkeling
    • Frontend ontwikkeling
    • Staff Augmentation
    • Backend ontwikkelaars
    • Cloud Ingenieurs
    • Gegevensingenieurs
    • Andere
    • QA ingenieurs

      Bronnen

    • Feiten en fabels over samenwerken met een externe partner voor softwareontwikkeling
    • Van de VS naar Europa: Waarom Amerikaanse startups besluiten naar Europa te verhuizen
    • Tech Offshore Ontwikkelingshubs Vergelijking: Tech Offshore Europa (Polen), ASEAN (Filippijnen), Eurazië (Turkije)
    • Wat zijn de grootste uitdagingen voor CTO's en CIO's?
    • The Codest
    • The Codest
    • The Codest
    • Privacy policy
    • Gebruiksvoorwaarden website

    Copyright © 2025 door The Codest. Alle rechten voorbehouden.

    nl_NLDutch
    en_USEnglish de_DEGerman sv_SESwedish da_DKDanish nb_NONorwegian fiFinnish fr_FRFrench pl_PLPolish arArabic it_ITItalian jaJapanese ko_KRKorean es_ESSpanish etEstonian elGreek nl_NLDutch