apispain 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 (4) hide show
  1. package/README.md +111 -0
  2. package/index.js +98 -0
  3. package/package.json +17 -0
  4. package/test.js +49 -0
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # apispain
2
+
3
+ SDK oficial para la [API de datos públicos de España](https://apispain.es).
4
+
5
+ Accede a licitaciones (PLACE), subvenciones (BDNS), sociedades (BORME) y normativa (BOE) con una sola librería.
6
+
7
+ ## Instalación
8
+
9
+ ```bash
10
+ npm install apispain
11
+ ```
12
+
13
+ ## Inicio rápido
14
+
15
+ ```js
16
+ import Apispain from 'apispain'
17
+
18
+ const client = new Apispain({ apiKey: 'tu-api-key' })
19
+
20
+ // Licitaciones abiertas
21
+ const { data } = await client.place.getLicitaciones({ abiertas: true, limit: 10 })
22
+ console.log(data[0].objeto)
23
+
24
+ // Subvenciones por CCAA
25
+ const subs = await client.bdns.getSubvenciones({ ccaa: 'Cataluña', limit: 5 })
26
+
27
+ // Sociedad por ID
28
+ const soc = await client.borme.getSociedadById('ES123456789')
29
+ ```
30
+
31
+ ## Constructor
32
+
33
+ ```js
34
+ new Apispain({
35
+ apiKey: string, // requerido
36
+ baseUrl: string, // default: 'https://api.apispain.es/v1'
37
+ timeout: number, // ms, default: 30000
38
+ })
39
+ ```
40
+
41
+ ## Namespaces
42
+
43
+ ### `client.place` — Licitaciones (PLACE)
44
+
45
+ | Método | Parámetros |
46
+ |--------|-----------|
47
+ | `getLicitaciones(params)` | `q`, `cpv`, `organo`, `abiertas`, `importeMin`, `importeMax`, `limit`, `cursor` |
48
+ | `getLicitacionById(id)` | — |
49
+
50
+ ### `client.bdns` — Subvenciones (BDNS)
51
+
52
+ | Método | Parámetros |
53
+ |--------|-----------|
54
+ | `getSubvenciones(params)` | `q`, `ccaa`, `sector`, `abiertas`, `importeMin`, `importeMax`, `limit`, `cursor` |
55
+ | `getSubvencionById(id)` | — |
56
+
57
+ ### `client.borme` — Registro Mercantil
58
+
59
+ | Método | Parámetros |
60
+ |--------|-----------|
61
+ | `getSociedades(params)` | `q`, `provincia`, `limit`, `cursor` |
62
+ | `getSociedadById(id)` | — |
63
+ | `getActos(params)` | `tipo`, `fechaDesde`, `limit`, `cursor` |
64
+
65
+ ### `client.boe` — Boletín Oficial del Estado
66
+
67
+ | Método | Parámetros |
68
+ |--------|-----------|
69
+ | `getDocumentos(params)` | `q`, `seccion`, `fecha`, `limit`, `cursor` |
70
+ | `getDocumentoById(id)` | — |
71
+
72
+ ### `client.webhooks`
73
+
74
+ | Método | Descripción |
75
+ |--------|-------------|
76
+ | `list()` | Lista tus webhooks |
77
+ | `create({ url, eventos })` | Crea un webhook |
78
+ | `delete(id)` | Elimina un webhook |
79
+
80
+ ## Manejo de errores
81
+
82
+ ```js
83
+ import { ApispainError } from 'apispain'
84
+
85
+ try {
86
+ const data = await client.place.getLicitaciones({ limit: 20 })
87
+ } catch (err) {
88
+ if (err instanceof ApispainError) {
89
+ console.error(err.status) // 401, 429, 500...
90
+ console.error(err.message) // mensaje legible
91
+ console.error(err.body) // body JSON completo
92
+ }
93
+ }
94
+ ```
95
+
96
+ ## Paginación
97
+
98
+ Todas las listas devuelven `{ data, pagination: { nextCursor } }`. Pasa `cursor` en la siguiente llamada:
99
+
100
+ ```js
101
+ let cursor
102
+ do {
103
+ const res = await client.bdns.getSubvenciones({ limit: 100, cursor })
104
+ processar(res.data)
105
+ cursor = res.pagination.nextCursor
106
+ } while (cursor)
107
+ ```
108
+
109
+ ## Licencia
110
+
111
+ MIT
package/index.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * apispain — SDK oficial para la API de datos públicos de España
3
+ * https://apispain.es
4
+ */
5
+
6
+ export class ApispainError extends Error {
7
+ constructor(message, status, body) {
8
+ super(message)
9
+ this.name = 'ApispainError'
10
+ this.status = status
11
+ this.body = body
12
+ }
13
+ }
14
+
15
+ export class Apispain {
16
+ #apiKey
17
+ #baseUrl
18
+ #timeout
19
+
20
+ constructor({ apiKey, baseUrl = 'https://api.apispain.es/v1', timeout = 30000 } = {}) {
21
+ if (!apiKey) throw new Error('apiKey is required')
22
+ this.#apiKey = apiKey
23
+ this.#baseUrl = baseUrl.replace(/\/$/, '')
24
+ this.#timeout = timeout
25
+
26
+ this.borme = {
27
+ getSociedades: (params) => this.#request('/borme/sociedades', params),
28
+ getSociedadById: (id) => this.#request(`/borme/sociedades/${id}`),
29
+ getActos: (params) => this.#request('/borme/actos', params),
30
+ }
31
+
32
+ this.boe = {
33
+ getDocumentos: (params) => this.#request('/boe/documentos', params),
34
+ getDocumentoById: (id) => this.#request(`/boe/documentos/${id}`),
35
+ }
36
+
37
+ this.bdns = {
38
+ getSubvenciones: (params) => this.#request('/bdns/subvenciones', params),
39
+ getSubvencionById: (id) => this.#request(`/bdns/subvenciones/${id}`),
40
+ }
41
+
42
+ this.place = {
43
+ getLicitaciones: (params) => this.#request('/place/licitaciones', params),
44
+ getLicitacionById: (id) => this.#request(`/place/licitaciones/${id}`),
45
+ }
46
+
47
+ this.webhooks = {
48
+ list: () => this.#request('/webhooks'),
49
+ create: (body) => this.#request('/webhooks', null, 'POST', body),
50
+ delete: (id) => this.#request(`/webhooks/${id}`, null, 'DELETE'),
51
+ }
52
+ }
53
+
54
+ async #request(path, params = null, method = 'GET', body = null) {
55
+ const controller = new AbortController()
56
+ const timer = setTimeout(() => controller.abort(), this.#timeout)
57
+
58
+ let url = `${this.#baseUrl}${path}`
59
+ if (params) {
60
+ const qs = new URLSearchParams(
61
+ Object.fromEntries(Object.entries(params).filter(([, v]) => v !== undefined && v !== null))
62
+ )
63
+ if (qs.toString()) url += `?${qs}`
64
+ }
65
+
66
+ try {
67
+ const res = await fetch(url, {
68
+ method,
69
+ headers: {
70
+ 'Authorization': `Bearer ${this.#apiKey}`,
71
+ 'Content-Type': 'application/json',
72
+ 'User-Agent': 'apispain-sdk/1.0.0',
73
+ },
74
+ body: body ? JSON.stringify(body) : undefined,
75
+ signal: controller.signal,
76
+ })
77
+
78
+ const data = await res.json().catch(() => ({}))
79
+
80
+ if (!res.ok) {
81
+ throw new ApispainError(
82
+ data?.message ?? `HTTP ${res.status}`,
83
+ res.status,
84
+ data
85
+ )
86
+ }
87
+
88
+ return data
89
+ } catch (err) {
90
+ if (err.name === 'AbortError') throw new ApispainError('Request timeout', 408, null)
91
+ throw err
92
+ } finally {
93
+ clearTimeout(timer)
94
+ }
95
+ }
96
+ }
97
+
98
+ export default Apispain
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "apispain",
3
+ "version": "1.0.0",
4
+ "description": "SDK oficial para la API de datos públicos de España (BORME, BOE, BDNS, PLACE)",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "keywords": ["spain", "api", "borme", "boe", "bdns", "place", "licitaciones", "subvenciones"],
11
+ "author": "apispain",
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/apispain/sdk"
16
+ }
17
+ }
package/test.js ADDED
@@ -0,0 +1,49 @@
1
+ import { Apispain, ApispainError } from './index.js'
2
+
3
+ let passed = 0
4
+ let failed = 0
5
+
6
+ function ok(label) { console.log(` ✓ ${label}`); passed++ }
7
+ function fail(label, err) { console.log(` ✗ ${label}: ${err}`); failed++ }
8
+
9
+ // Test 1: constructor throws without apiKey
10
+ try {
11
+ new Apispain()
12
+ fail('throws without apiKey', 'no error thrown')
13
+ } catch (e) {
14
+ if (e.message === 'apiKey is required') ok('throws without apiKey')
15
+ else fail('throws without apiKey', e.message)
16
+ }
17
+
18
+ // Test 2: ApispainError has .status
19
+ try {
20
+ const err = new ApispainError('test error', 401, { message: 'Unauthorized' })
21
+ if (err.status === 401 && err.name === 'ApispainError') ok('ApispainError has .status and .name')
22
+ else fail('ApispainError shape', `status=${err.status} name=${err.name}`)
23
+ } catch (e) {
24
+ fail('ApispainError', e.message)
25
+ }
26
+
27
+ // Test 3: client instantiates correctly
28
+ try {
29
+ const client = new Apispain({ apiKey: 'test-key' })
30
+ const hasAll = ['borme', 'boe', 'bdns', 'place', 'webhooks'].every(ns => typeof client[ns] === 'object')
31
+ if (hasAll) ok('client has all namespaces')
32
+ else fail('client namespaces', 'missing namespace')
33
+ } catch (e) {
34
+ fail('client instantiation', e.message)
35
+ }
36
+
37
+ // Test 4: real API call with invalid key → 401 ApispainError
38
+ console.log('\n [network] testing real API with invalid key...')
39
+ try {
40
+ const client = new Apispain({ apiKey: 'invalid-key-test', baseUrl: 'https://api.apispain.es/v1' })
41
+ await client.bdns.getSubvenciones({ limit: 1 })
42
+ fail('should throw on invalid key', 'no error thrown')
43
+ } catch (e) {
44
+ if (e instanceof ApispainError && e.status === 401) ok(`real API returns 401 ApispainError (message: "${e.message}")`)
45
+ else fail('real API 401', `got ${e.constructor.name}: ${e.message}`)
46
+ }
47
+
48
+ console.log(`\n ${passed} passed, ${failed} failed\n`)
49
+ if (failed > 0) process.exit(1)