Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
67cb50e
feat: add search tables migration and update db config for local post…
elainefan331 May 7, 2026
7432b35
feat: add incremental sync script to populate ioviews and iolinks fro…
elainefan331 May 8, 2026
f686418
feat: refactor incremental sync with transactions, concurrency, and e…
elainefan331 May 8, 2026
9d69223
feat: add ioviews unique constraint migration and disable sequelize l…
elainefan331 May 8, 2026
627e598
feat: replace CGI search with PostgreSQL query in searchAllDatabases …
elainefan331 May 11, 2026
207c634
fix: widen ioviews and iolinks text columns to handle longer values
elainefan331 May 11, 2026
4490af6
fix: make suggested databases refresh on Search click
elainefan331 May 11, 2026
5966b09
feat: modality-aware combobox for Data type filter
elainefan331 May 11, 2026
d101e5c
feat(search): add draggable age-range slider at top of subject filters
elainefan331 May 11, 2026
88474cc
feat(search): add placeholder hints to task/session/run keyword fields
elainefan331 May 11, 2026
b9be29f
feat(search): pair min/max count fields on one row and tighten gaps(s…
elainefan331 May 11, 2026
df73e1c
feat(search): show run count in subject card
elainefan331 May 11, 2026
44635bd
feat(backend): add file type filter to search using iolinks table
elainefan331 May 12, 2026
1b0aa65
feat(search): add multi-select File types filter (dataset-level)
elainefan331 May 12, 2026
9f826d4
fix(search): age slider per-handle clearing + non-BIDS file-type warning
elainefan331 May 12, 2026
38fc1cb
fix(search): highlight each word of multi-word keyword independently
elainefan331 May 12, 2026
bebd036
fix(search): keyword highlight reads from appliedFilters, not formData
elainefan331 May 12, 2026
fba0240
feat: add file download endpoints for dataset search results
elainefan331 May 13, 2026
51960cf
feat(search): show matching files in dataset card with selective and …
elainefan331 May 13, 2026
5677c74
fix(search): use plainto_tsquery + normalize ILIKE separators for key…
elainefan331 May 13, 2026
bf1abe3
fix(search): match dataset display name via json->>'name' ILIKE
elainefan331 May 13, 2026
6fce063
feat(search): add Mac/Linux and Windows script options to file download
elainefan331 May 14, 2026
8be3fa1
fix(dataset-detail): keep all downloaded files in one visible folder
elainefan331 May 15, 2026
e21c661
feat(dataset-detail): three script formats for download all files button
elainefan331 May 15, 2026
909cd36
feat(sync): pull database list from registry
elainefan331 May 20, 2026
0cee8b6
feat(search): add dataset-level modality filter with AND/OR mode
elainefan331 May 21, 2026
601d90b
feat(search): add tooltip to Subject-Level Filters explaining result …
elainefan331 May 22, 2026
5aab808
fix(sync): reject malformed file extensions from CouchDB links view
elainefan331 May 22, 2026
f834a47
style(search): match dataset modality filter UI to file types field
elainefan331 May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions backend/config/config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
require("dotenv").config();

module.exports = {
// development: {
// dialect: "sqlite",
// storage: "./database.sqlite",
// logging: console.log,
// },
development: {
dialect: "sqlite",
storage: "./database.sqlite",
logging: console.log,
dialect: "postgres",
host: "localhost",
port: 5432,
database: "neurojson_dev",
username: process.env.DB_USER_LOCAL,
password: process.env.DB_PASSWORD_LOCAL,
logging: false,
},
test: {
dialect: "sqlite",
Expand Down
130 changes: 130 additions & 0 deletions backend/migrations/20260507145253-create-search-tables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"use strict";

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
// ioviews table
await queryInterface.createTable("ioviews", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
dbname: {
type: Sequelize.STRING(30),
allowNull: true,
},
dsname: {
type: Sequelize.STRING(30),
allowNull: true,
},
subj: {
type: Sequelize.STRING(12),
allowNull: true,
},
view: {
type: Sequelize.STRING(12),
allowNull: true,
},
json: {
type: Sequelize.JSONB,
allowNull: true,
},
search_vector: {
type: Sequelize.DataTypes.TSVECTOR,
allowNull: true,
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal("CURRENT_TIMESTAMP"),
},
});

// ioviews indexes
await queryInterface.addIndex("ioviews", ["view"], {
name: "idx_ioviews_view",
});
await queryInterface.addIndex("ioviews", ["dbname"], {
name: "idx_ioviews_dbname",
});
await queryInterface.addIndex("ioviews", ["updated_at"], {
name: "idx_ioviews_updated_at",
});

// GIN indexes need raw query (not supported by addIndex)
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_ioviews_search
ON ioviews USING GIN(search_vector);
CREATE INDEX IF NOT EXISTS idx_ioviews_json
ON ioviews USING GIN(json);
`);

// iolinks table
await queryInterface.createTable("iolinks", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
dbname: {
type: Sequelize.STRING(30),
allowNull: true,
},
dsname: {
type: Sequelize.STRING(30),
allowNull: true,
},
subj: {
type: Sequelize.TEXT,
allowNull: true,
},
view: {
type: Sequelize.TEXT,
allowNull: true,
},
json: {
type: Sequelize.JSONB,
allowNull: true,
},
});

// iolinks indexes
await queryInterface.addIndex("iolinks", ["view"], {
name: "idx_iolinks_view",
});
await queryInterface.addIndex("iolinks", ["dbname"], {
name: "idx_iolinks_dbname",
});
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_iolinks_json
ON iolinks USING GIN(json);
`);

// sync_state table
await queryInterface.createTable("sync_state", {
dbname: {
type: Sequelize.STRING(30),
primaryKey: true,
allowNull: false,
},
last_seq: {
type: Sequelize.TEXT,
allowNull: true,
},
synced_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal("CURRENT_TIMESTAMP"),
},
});
},

async down(queryInterface, Sequelize) {
await queryInterface.dropTable("ioviews");
await queryInterface.dropTable("iolinks");
await queryInterface.dropTable("sync_state");
},
};
20 changes: 20 additions & 0 deletions backend/migrations/20260508195500-add-ioviews-unique-constraint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use strict";

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
// Required by upsertIoview's ON CONFLICT (dbname, dsname, subj, view).
await queryInterface.addConstraint("ioviews", {
fields: ["dbname", "dsname", "subj", "view"],
type: "unique",
name: "ioviews_dbname_dsname_subj_view_unique",
});
},

async down(queryInterface, Sequelize) {
await queryInterface.removeConstraint(
"ioviews",
"ioviews_dbname_dsname_subj_view_unique"
);
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use strict";

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
// VARCHAR(n) → TEXT is a metadata-only change in Postgres (no table rewrite,
// no need to drop the unique constraint or indexes).
await queryInterface.changeColumn("ioviews", "dbname", {
type: Sequelize.TEXT,
allowNull: true,
});
await queryInterface.changeColumn("ioviews", "dsname", {
type: Sequelize.TEXT,
allowNull: true,
});
await queryInterface.changeColumn("ioviews", "subj", {
type: Sequelize.TEXT,
allowNull: true,
});
await queryInterface.changeColumn("iolinks", "dbname", {
type: Sequelize.TEXT,
allowNull: true,
});
await queryInterface.changeColumn("iolinks", "dsname", {
type: Sequelize.TEXT,
allowNull: true,
});
await queryInterface.changeColumn("sync_state", "dbname", {
type: Sequelize.TEXT,
allowNull: false,
});
},

async down(queryInterface, Sequelize) {
await queryInterface.changeColumn("ioviews", "dbname", {
type: Sequelize.STRING(30),
allowNull: true,
});
await queryInterface.changeColumn("ioviews", "dsname", {
type: Sequelize.STRING(30),
allowNull: true,
});
await queryInterface.changeColumn("ioviews", "subj", {
type: Sequelize.STRING(12),
allowNull: true,
});
await queryInterface.changeColumn("iolinks", "dbname", {
type: Sequelize.STRING(30),
allowNull: true,
});
await queryInterface.changeColumn("iolinks", "dsname", {
type: Sequelize.STRING(30),
allowNull: true,
});
await queryInterface.changeColumn("sync_state", "dbname", {
type: Sequelize.STRING(30),
allowNull: false,
});
},
};
3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"migrate:undo:all": "npx sequelize-cli db:migrate:undo:all",
"seed": "npx sequelize-cli db:seed:all",
"seed:undo": "npx sequelize-cli db:seed:undo:all",
"db:reset": "npx sequelize-cli db:migrate:undo:all && npx sequelize-cli db:migrate"
"db:reset": "npx sequelize-cli db:migrate:undo:all && npx sequelize-cli db:migrate",
"sync": "node sync/incrementalSync.js"
},
"keywords": [
"express",
Expand Down
Loading
Loading