Scrydon

Sources de données

Créez des sources de données déclaratives en mode poll et livrez-les dans un Scrydon Pack — aucun code d'exécution requis

Cet artefact est livré dans un Pack. Pour le cycle de vie partagé — installation, pack build, téléversement — voir Packs & SDK d'authoring.

Une Source de données déclarative est une source de table en mode poll encodée entièrement sous forme de configuration JSON sérialisable — une spécification de requête HTTP, un sélecteur itemsPath, une table de correspondance de champs et une liste de colonnes. Vous la créez une fois avec defineDataSource ; le runtime générique de poll de la plateforme l'exécute à chaque tick sans exécuter de code fourni par le client.

Les sources de données livrées dans un Pack sont du JSON pur — l'archive contient la spécification de requête, la correspondance et les définitions de colonnes, jamais du code. Le runtime générique de poll (fetch → select → map → validate) réside dans la plateforme et est invoqué depuis le manifeste à chaque tick.

Quand utiliser ce SDK

Utilisez le SDK d'authoring de sources de données lorsque :

  • Vous souhaitez extraire des lignes d'une API REST publique selon un calendrier et les rendre disponibles sous forme de table typée dans Scrydon.
  • Vous avez besoin que la définition de source réside dans un Pack afin que le re-téléversement du pack rafraîchisse également la source (même contrat d'idempotence que les ontologies et les flux de processus).
  • Vous construisez un Pack de démonstration ou de démarrage et souhaitez que les tables de données viennent précâblées sans configuration manuelle.

N'utilisez pas ce SDK si :

  • Votre logique d'ingestion est conditionnelle, avec état, ou nécessite du code (curseurs de pagination, rafraîchissement de token OAuth, signature de charge utile personnalisée). Pour ces cas, créez une source à code livré dans le monorepo de la plateforme — elle utilise le même appel defineDataSource mais avec une fonction produce().
  • La destination n'est pas une table. Les chemins d'ingestion non tabulaires sont hors du périmètre de ce SDK aujourd'hui.

Contrainte clé : les packs contiennent du JSON pur — jamais de fonctions. Le bundler supprime toutes les fonctions produce() lors de la sérialisation. Une source packagable doit donc être entièrement déclarative — chaque champ de la définition de source doit survivre à l'aller-retour JSON.parse(JSON.stringify(...)) intact.

Installation

bun add -d @scrydon/sdk-authoring zod
npm install --save-dev @scrydon/sdk-authoring zod
import { defineDataSource } from '@scrydon/sdk-authoring/integrations'

Anatomie d'une source de données déclarative

Une source de données déclarative est composée de cinq blocs :

BlocChamp(s)Rôle
requesturl, method, headers, query, authRefDécrit l'appel HTTP que le runtime effectue à chaque tick. url doit commencer par https://. Les identifiants sont référencés par authRef (un id de connexion d'identifiants) — jamais inline.
response.itemsPathitemsPathUn chemin point/crochet (par ex. $.ac, data.items) depuis l'enveloppe de réponse JSON jusqu'au tableau d'objets de lignes. Le $. initial est optionnel.
mappingRecord<columnName, FieldMapping>Associe chaque colonne de sortie à un champ source, en appliquant optionnellement l'une des quatre transformations bornées.
filterrequireNonNullSupprime les lignes candidates avant le mapping si l'un des chemins de champ source listés est null ou undefined.
table.columnsTableColumnDef[]Déclare le schéma de sortie. Les noms et types de colonnes pilotent le validateur de lignes — aucun table.schema Zod séparé n'est nécessaire pour les sources déclaratives.

Le DSL de mapping

Chaque entrée mapping copie soit un chemin directement, soit applique une transformation nommée. L'ensemble des transformations est borné et auditable — l'ajout d'une nouvelle transformation nécessite une modification de code dans la plateforme après revue, pas de données de pack. Les expressions arbitraires, l'eval et le code en sandbox ne sont intentionnellement pas supportés.

TransformationCe qu'elle faitExemple args
trim_to_nullSupprime les espaces en début/fin ; transforme une chaîne vide ou non-chaîne en null. "RCH123 ""RCH123", " "null, undefinednull.(aucun requis)
number_or_nullPasse les nombres finis sans modification ; transforme les chaînes, NaN, Infinity et undefined en null.(aucun requis)
value_mapAssocie des clés de chaîne littérales via un dictionnaire map ; passthrough: "number" passe les valeurs numériques sans modification ; tout le reste se replie sur default.{ map: { ground: 0 }, passthrough: "number", default: null }
iso_from_epoch_offsetCombine un horodatage de base au niveau de l'enveloppe (basePath est résolu par rapport à la racine de la réponse) avec un décalage signé par ligne, et retourne une chaîne ISO 8601. Utile pour les API qui signalent une horloge now absolue plus des valeurs seen en secondes-passées par élément.{ basePath: "$.now", baseUnit: "ms", offsetUnit: "s", direction: "subtract" }

Le cas difficile de iso_from_epoch_offset

Certaines API REST — la famille ADS-B en est l'exemple canonique — ne retournent pas d'horodatages absolus par ligne. L'enveloppe porte un seul champ now (millisecondes epoch) et chaque ligne porte un champ seen (secondes passées). La transformation iso_from_epoch_offset établit le pont entre les deux :

seenAt = new Date(envelope.now - row.seen * 1000).toISOString()

Configurez-la comme suit :

seenAt: {
  path: "seen",                              // champ de décalage par ligne
  transform: "iso_from_epoch_offset",
  args: {
    basePath: "$.now",                       // champ d'enveloppe — résolu par rapport à la racine de la réponse
    baseUnit: "ms",                          // $.now est en millisecondes epoch
    offsetUnit: "s",                         // row.seen est en secondes
    direction: "subtract",                   // now − seen → heure absolue
  },
},

Si row.seen est absent ou non numérique, le runtime prend par défaut un décalage de 0 (heure de base exacte), conformément à la convention de la source de code seenSecondsAgo = typeof raw.seen === "number" ? raw.seen : 0.

Un exemple complet

Ce qui suit est la vraie source adsb-lol-military-declarative livrée dans @scrydon/sdk-authoring. Elle extrait les positions d'aéronefs militaires depuis https://api.adsb.lol/v2/mil et les associe à une table typée aircraft_position. Le test de parité golden dans le monorepo affirme que cela produit des lignes identiques octet par octet à la source de code équivalente.

import { defineDataSource } from '@scrydon/sdk-authoring/integrations'

export const adsbLolMilitaryDeclarative = defineDataSource({
  kind: "table",
  id: "adsb-lol-military-declarative",
  vendor: "adsb-lol",
  displayName: "ADS-B Lol — Military Aircraft (declarative)",
  scope: "global",
  table: {
    name: "aircraft_position",
    primaryKey: ["icao24", "seenAt"],
    timestampColumn: "seenAt",
    columns: [
      { name: "icao24",           dataType: "string",  isPrimaryKey: true },
      { name: "callsign",         dataType: "string",  nullable: true },
      { name: "registration",     dataType: "string",  nullable: true },
      { name: "aircraftType",     dataType: "string",  nullable: true },
      { name: "category",         dataType: "string",  nullable: true },
      { name: "latitude",         dataType: "decimal" },
      { name: "longitude",        dataType: "decimal" },
      { name: "altitudeFeet",     dataType: "int",     nullable: true },
      { name: "groundSpeedKnots", dataType: "double",  nullable: true },
      { name: "heading",          dataType: "double",  nullable: true },
      { name: "seenAt",           dataType: "timestamp", isPrimaryKey: true },
    ],
  },
  ingest: {
    mode: "poll",
    intervalSec: 60,
    minIntervalSec: 30,
    request: {
      url: "https://api.adsb.lol/v2/mil",
      method: "GET",
      headers: { accept: "application/json" },
    },
    response: { itemsPath: "$.ac" },
    filter: {
      // Supprime les lignes sans hex (ICAO), lat ou lon — reprend la garde de la source de code.
      requireNonNull: ["hex", "lat", "lon"],
    },
    mapping: {
      icao24:           { path: "hex" },
      // "RCH123 " → "RCH123", "   " → null. Reprend raw.flight?.trim() || null
      callsign:         { path: "flight", transform: "trim_to_null" },
      // undefined → null. Reprend raw.r ?? null
      registration:     { path: "r" },
      // undefined → null. Reprend raw.t ?? null
      aircraftType:     { path: "t" },
      // undefined → null. Reprend raw.category ?? null
      category:         { path: "category" },
      latitude:         { path: "lat" },
      longitude:        { path: "lon" },
      // "ground" → 0, nombre → passthrough, sinon → null. Reprend parseAltitude()
      altitudeFeet: {
        path: "alt_baro",
        transform: "value_map",
        args: { map: { ground: 0 }, passthrough: "number", default: null },
      },
      // nombre fini → lui-même, chaîne/NaN/undefined → null
      groundSpeedKnots: { path: "gs",    transform: "number_or_null" },
      heading:          { path: "track", transform: "number_or_null" },
      // new Date(envelope.now - row.seen * 1000).toISOString()
      seenAt: {
        path: "seen",
        transform: "iso_from_epoch_offset",
        args: {
          basePath: "$.now",
          baseUnit: "ms",
          offsetUnit: "s",
          direction: "subtract",
        },
      },
    },
  },
})

defineDataSource est une fonction d'identité à l'exécution — elle valide le manifeste via le schéma Zod DataSourceManifestSchema et dérive un validateur de lignes depuis table.columns. La définition émise est de la pure donnée : pas de fonction produce(), pas de fermetures, sérialisable tel quel dans data-source-<slug>/manifest.json.

Modes d'écriture

Chaque tick produit un lot de lignes ; ingest.writeMode contrôle la façon dont ce lot atterrit dans la table. C'est uniquement à l'installation — il provisionne le modèle de clé StarRocks lors de la création de la table et ne peut pas être modifié par la suite (le changer nécessite une réinstallation du pack avec un manifeste mis à jour). La valeur par défaut est upsert.

writeModeClé de déduplicationLignes conservées par entitéÀ utiliser pour
upsert (par défaut)table.primaryKey (identité)1 — la plus récente uniquementFlux « état actuel ». Chaque poll écrase la ligne de l'entité en place ; pas d'historique.
changed-onlyprimaryKey + un hash de contenu synthétiséN — une par état distinctDonnées à évolution lente où vous souhaitez un historique mais pas une ligne par poll. Les polls identiques consécutifs se replient sur la même clé ; une ligne n'est écrite que lorsqu'une valeur change réellement.
appendaucunechaque pollFlux d'événements bruts où chaque observation compte, y compris les répétitions exactes.
replaceprimaryKeydernier instantané completPetites tables de référence republikes en entier à chaque tick (truncate-then-load).

changed-only vs upsert. Les deux effectuent un upsert en interne, mais ils dédupliquent sur des clés différentes. upsert conserve une ligne toujours courante par entité (pas d'historique). changed-only conserve une ligne par état distinct que l'entité a traversé, en sautant les polls où rien n'a changé. Pour la télémétrie en évolution continue (par ex. la position en direct d'un aéronef, où la latitude/longitude change à chaque poll), changed-only se comporte presque comme append — le hash de contenu diffère à chaque tick. Là, le levier de croissance est la rétention, pas le mode d'écriture.

ingest: {
  mode: "poll",
  intervalSec: 60,
  writeMode: "changed-only", // omettre pour le défaut "upsert"
  // ...request, response, mapping
}

Rétention

Pour les tables de séries temporelles qui croissent continuellement, déclarez table.ttlSec aux côtés de table.timestampColumn. La plateforme partitionne la table StarRocks par jour sur la colonne d'horodatage et conserve uniquement les ceil(ttlSec / 86400) partitions quotidiennes les plus récentes — les partitions plus anciennes sont supprimées automatiquement.

table: {
  name: "aircraft_position",
  primaryKey: ["icao24", "seenAt"],
  timestampColumn: "seenAt",   // la colonne sur laquelle les partitions sont découpées
  ttlSec: 604800,              // conserver ~7 jours de partitions
  columns: [ /* ... */ ],
}

ttlSec est la valeur par défaut d'authoring. Un administrateur d'organisation peut remplacer la rétention effective par source depuis Paramètres → Plateforme → Sources de données (ouvrez le panneau de détail d'une source et modifiez « Rétention ») — le remplacement est appliqué en direct, sans reconstruction de table. Le panneau de détail affiche également le mode d'écriture de la source (lecture seule) et son historique de synchronisation récent, et renvoie directement à la table sous-jacente.

La Zone de danger du panneau de détail propose également Supprimer la source de données : elle supprime la table sous-jacente et toutes ses lignes, efface la source, et — lorsque le pack d'origine n'a rien contribué d'autre — supprime également le pack. Si le pack livre d'autre contenu, la source est supprimée mais le pack est conservé (désinstallez-le depuis Paramètres → Packs). La suppression est irréversible ; réinstaller le pack recrée la source.

La rétention limite la croissance de la table pour tout mode d'écriture, et c'est l'outil approprié lorsque changed-only ne peut pas aider (télémétrie en évolution continue). Elle est indépendante du mode d'écriture : vous pouvez combiner append ou changed-only avec ttlSec pour conserver l'historique mais le limiter à une fenêtre glissante.

Structure du bundle

Un Pack qui livre une source de données ajoute un sous-répertoire data-source-<slug>/ par source, chacun contenant un seul manifest.json. Plusieurs sources de données peuvent coexister dans le même Pack — data-source est un type de contenu répétable, comme workflow.

<bundle>.scrydon-pack.tar.gz
├── pack.json                               # PackBundleManifestSchema
└── data-source-adsb-lol-military/
    └── manifest.json                       # DataSourceManifestSchema (JSON pur, sans code)

Le pack.json de niveau supérieur liste la source de données comme entrée dans contents[] et inclut "data-source" dans installOrder :

import { defineScrydonPack } from '@scrydon/sdk-authoring/packs'
import { adsbLolMilitaryDeclarative } from './data-source-adsb-lol-military'

export default defineScrydonPack({
  manifestVersion: 1,
  package: {
    id: 'adsb-lol-military',
    name: 'ADS-B Lol Military Aircraft',
    version: '1.0.0',
  },
  contents: [
    {
      kind: 'data-source',
      path: 'data-source-adsb-lol-military',
      version: '1.0.0',
      required: true,
    },
  ],
  installOrder: ['data-source'],
  metadata: { isSystemPack: false, isDemoPack: false, tags: ['adsb', 'aircraft'] },
})

Build, inspection, téléversement

Créez chaque source de données avec defineDataSource. Composez le Pack avec defineScrydonPack et ajoutez une entrée contents[] par source avec kind: "data-source".

bunx @scrydon/sdk-authoring pack build src/pack.ts --outDir dist
# → dist/<package.id>-<package.version>.scrydon-pack.tar.gz
bunx @scrydon/sdk-authoring pack inspect dist/adsb-lol-military-1.0.0.scrydon-pack.tar.gz

L'inspecteur liste chaque sous-répertoire, valide chaque manifeste par rapport à DataSourceManifestSchema, et signale toute erreur de mapping ou de colonne avant le téléversement.

Les packs de sources de données se téléversent dans Paramètres → Packs dans l'application de plateforme. Le téléversement admet le pack dans le catalogue de packs de l'organisation. Aucune ligne data_source n'est créée pour l'instant — les sources de données diffèrent à l'Étape 2 pour que l'utilisateur du workspace puisse choisir dans quel environnement elles se matérialisent.

Équivalent programmatique :

curl -X POST "$AGENTIC_URL/api/packs/import?organizationId=$ORG_ID" \
  -H "Cookie: $SESSION_COOKIE" \
  -F "file=@dist/adsb-lol-military-1.0.0.scrydon-pack.tar.gz"

La route retourne l'id de l'entrée du catalogue ; dataSources.installedIds est vide à ce stade par conception.

Dans apps/analytics → Marketplace, depuis un workspace + environnement, sélectionnez la source de données depuis un pack catalogué et cliquez sur Installer dans cet environnement. La plateforme matérialise la ligne data_source limitée à votre environnement et commence à interroger selon la cadence intervalSec configurée — généralement dans l'espace d'une minute. Gérez la source depuis Analytics → Sources de données.

Sources de données nécessitant des identifiants

Certaines API nécessitent un compte ou une clé API. Pour ces cas, utilisez le champ request.authRef — une référence nommée vers une connexion d'identifiants configurée dans les paramètres de votre organisation. Le manifeste ne contient jamais de secret inline ; authRef est un pointeur, pas une valeur. (Le DataSourceManifestSchema rejette toute tentative d'intégrer directement un en-tête Authorization ou une clé API dans le manifeste.)

ingest: {
  mode: "poll",
  intervalSec: 300,
  request: {
    url: "https://api.example.com/v1/records",
    method: "GET",
    authRef: "example-api-key",   // référence nommée — PAS un token inline
  },
  // ...
}

Déroulement de la connexion :

  1. Vous (ou votre administrateur d'organisation) connectez le compte dans les paramètres de l'organisation, créant une connexion d'identifiants activée nommée "example-api-key".
  2. À chaque tick, la plateforme résout cette connexion côté serveur et attache les identifiants à la requête sortante — votre manifeste ne voit jamais le secret.
  3. Si aucune connexion activée n'existe encore, le tick retourne HTTP 412 data_source_connection_required — un signal clair de connecter d'abord le compte. Aucune donnée n'est extraite, et aucune erreur n'est silencieusement absorbée.
  4. Une fois qu'une connexion est activée, les ticks suivants la résolvent automatiquement et s'exécutent.

Pas de authRef ? Une source sans authRef (API publique, pas d'authentification nécessaire) s'exécute immédiatement — aucune configuration d'identifiants requise.

La valeur que vous définissez pour authRef est le nom de connexion que votre administrateur d'organisation crée dans les paramètres. Coordonnez le nom entre l'auteur du pack et l'administrateur d'organisation — si les noms ne correspondent pas, les ticks retournent 412 jusqu'à ce que la connexion soit créée avec le nom attendu.

Sécurité

Garde de sortie : le runtime générique de poll applique une garde de sortie SSRF avant chaque fetch. Les URL de requête doivent utiliser https:// — le http:// simple est rejeté. Les requêtes vers des adresses de loopback (127.x.x.x, ::1), des adresses link-local (169.254.x.x), des plages privées (10.x, 172.16–31.x, 192.168.x), et les préfixes ULA IPv6 (fd00::/8) sont bloquées au niveau de la forme de l'URL avant qu'une connexion soit ouverte.

Remarque : la garde v1 est basée sur la forme de l'hôte/scheme. Un nom d'hôte qui se résout en DNS vers une IP privée au moment du fetch n'est pas bloqué par la garde v1 — l'épinglage d'IP résolue est prévu pour une prochaine version.

Identifiants par référence uniquement : le champ request.authRef accepte un id de connexion d'identifiants (un pointeur vers une connexion Tier-1 stockée dans le secret store de la plateforme). N'intégrez jamais une clé API, un token Bearer ou la valeur d'un en-tête Authorization directement dans le manifeste. Le DataSourceManifestSchema utilise .strict() sur le bloc request et rejette tout champ au-delà de url, method, headers, query et authRef — les champs parasites comme apiKey ou secret provoquent une erreur de validation lors du build.

Limites de ressources appliquées par tick :

LimiteDéfaut
Corps de la réponse8 Mo
Nombre de lignes10 000 lignes
Délai d'expiration du fetch15 secondes

Pour aller plus loin

Sur cette page

Sur cette page