Skip to content

Instantly share code, notes, and snippets.

@colinux
Last active March 25, 2026 14:39
Show Gist options
  • Select an option

  • Save colinux/a2ea5808fd9fe57578c192abe2731261 to your computer and use it in GitHub Desktop.

Select an option

Save colinux/a2ea5808fd9fe57578c192abe2731261 to your computer and use it in GitHub Desktop.
# Import a procedure structure from production via GraphQL API
# Works for public procedures (opendata: true) or with API_PRODUCTION_TOKEN for private procedures
#
# Usage:
# procedure = import_procedure_from_production(12345) # Uses ENV['DS_EMAIL']
# procedure = import_procedure_from_production(12345)
#
def import_procedure_from_production(procedure_number)
# Find local administrator using the same method as other helpers
admin = Administrateur.by_email(ENV.fetch("DS_EMAIL"))
# Fetch procedure data from production GraphQL API
graphql_data = fetch_procedure_from_graphql(procedure_number)
# Create procedure locally
procedure = nil
ActiveRecord::Base.transaction do
procedure = create_procedure_from_graphql_data(graphql_data, admin)
end
procedure
rescue => e
Rails.logger.error("Failed to import procedure #{procedure_number}: #{e.message}")
Rails.logger.error(e.backtrace.join("\n"))
raise
end
private
def fetch_procedure_from_graphql(procedure_number)
query_id = 'ds-query-v2'
operation_name = 'getDemarcheDescriptor'
url = 'https://demarche.numerique.gouv.fr/api/v2/graphql' # rubocop:disable DS/ApplicationName
headers = {
'Content-Type' => 'application/json',
}
body = {
queryId: query_id,
operationName: operation_name,
variables: {
demarche: { number: procedure_number },
includeRevision: true,
},
}.to_json
response = Typhoeus.post(url, body: body, headers: headers, timeout: 30)
unless response.success?
raise "GraphQL request failed: #{response.code} - #{response.body}"
end
result = JSON.parse(response.body)
if result['errors']
raise "GraphQL errors: #{result['errors'].map { _1['message'] }.join(', ')}"
end
result.dig('data', 'demarcheDescriptor')
end
def create_procedure_from_graphql_data(graphql_data, admin)
# Find or create a service
service = Service.first
# Extract path from demarcheURL
# URL format: https://demarche.numerique.gouv.fr/commencer/nom-de-la-demarche
path = URI.parse(graphql_data['demarcheURL']).path.split('/').last
# Create procedure
procedure = Procedure.new(
libelle: graphql_data['title'],
description: graphql_data['description'],
organisation: 'Imported from production',
cadre_juridique: 'Imported from production',
# limitation: demarcheDescription does not expose for_individual
for_individual: true,
lien_site_web: 'https://example.com',
duree_conservation_dossiers_dans_ds: 3,
max_duree_conservation_dossiers_dans_ds: Expired::DEFAULT_DOSSIER_RENTENTION_IN_MONTH,
service:,
administrateurs: [admin]
)
procedure.draft_revision = procedure.revisions.build
procedure.save!
# Add TypeDeChamps from revision
revision = graphql_data['revision']
if revision && revision['champDescriptors']
champ_descriptors = revision['champDescriptors']
champ_descriptors.each do |descriptor|
create_type_de_champ_from_descriptor(procedure.draft_revision, descriptor)
end
end
# Publish procedure with path
procedure.publish_or_reopen!(admin, path)
procedure
end
def create_type_de_champ_from_descriptor(revision, descriptor, parent_stable_id: nil)
type_champ = map_graphql_type_to_rails_type_champ(descriptor['__typename'])
params = {
type_champ:,
libelle: descriptor['label'] || 'Champ',
description: descriptor['description'],
mandatory: descriptor['required'] || false,
private: false,
parent_stable_id:,
}
# Handle dropdown options
if descriptor['options'].present?
params[:drop_down_list_value] = descriptor['options'].join("\n")
end
tdc = revision.add_type_de_champ(params)
# Handle repetition children recursively
if type_champ == :repetition && descriptor['champDescriptors'].present?
descriptor['champDescriptors'].each do |child_descriptor|
create_type_de_champ_from_descriptor(revision, child_descriptor, parent_stable_id: tdc.stable_id)
end
end
tdc
rescue => e
puts "Failed to create TypeDeChamp #{descriptor['__typename']}: #{e.message}"
raise
end
def map_graphql_type_to_rails_type_champ(graphql_typename)
# Remove 'ChampDescriptor' suffix and convert to snake_case symbol
# Example: TextChampDescriptor -> text, DropDownListChampDescriptor -> drop_down_list
graphql_typename.sub('ChampDescriptor', '').underscore.to_sym
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment